MarkItDown
MarkItDown architecture: streams, format hints and converter selection
Follow the inspected MarkItDown source from local input through StreamInfo guesses, prioritized converters, Markdown normalization and explicit failure classes.
What you will learn
- StreamInfo guesses and converter priority jointly determine selection.
- Non-seekable input is buffered before conversion.
- Unsupported input and failed conversion attempts are separate outcomes.
Before you start
- Basic Python and command-line usage
- A non-sensitive document whose contents you can verify
Use the chapter checklist to explain and verify this part of a document ingestion workflow.
Key takeaways
- StreamInfo guesses and converter priority jointly determine selection.
- Non-seekable input is buffered before conversion.
- Unsupported input and failed conversion attempts are separate outcomes.
Inputs converge on streams and metadata
In the inspected revision, convert_local builds a StreamInfo value from the path, including filename and extension. It opens the input in binary mode, asks for stream-information guesses and passes both the stream and guesses into _convert. Format handling is therefore downstream of input acquisition, not embedded in the filename argument alone.
convert_stream accepts a binary stream. The implementation buffers a non-seekable stream completely before running the conversion pipeline. That behavior supports retries and format probes but matters for memory sizing: a streaming upload into this API does not imply bounded-memory extraction.
Selection is ordered and supports more than one guess
The core _convert method creates a stable priority-sorted list of registered converters. It visits the stream-information guesses plus an empty fallback guess, then asks each converter whether it accepts the input. This explains why an extension is a hint rather than the only decision variable.
Specific-format converters normally use priority zero, while generic text, HTML and ZIP converters use priority ten. Lower values are attempted earlier. Registration inserts new entries at the front, so equal-priority ordering favors the more recent registration after the stable sort. A custom plugin should choose its acceptance rules as carefully as its numeric priority.
Success and failure have observable contracts
The implementation asserts that acceptance checks leave the file position unchanged. An attempted conversion resets the stream position in a finally block. This allows a later candidate to examine the same input after a previous candidate failed, rather than seeing an already-consumed stream.
On success, the core trims trailing line whitespace and reduces long runs of blank lines before returning. If conversion attempts failed, it raises FileConversionException with recorded attempts. If no converter attempted the input, it raises UnsupportedFormatException. Callers can use that distinction to choose diagnosis instead of retrying everything.
Implementation steps
- 1
Read convert_local and convert_stream in the pinned core file.
- 2
Follow their calls into _get_stream_info_guesses and _convert.
- 3
Inspect priority sorting and the accepts/convert boundary.
- 4
Compare the two terminal error classes.
Copy-ready example
convert_local / convert_stream
-> binary stream + StreamInfo guesses
-> stable sort of registered converter priorities
-> accepts() -> convert() -> restore stream position
-> normalize Markdown -> result
-> attempted failures OR unsupported formatFrequently asked questions
Does the extension alone select a converter?
No. The inspected pipeline tries StreamInfo guesses against prioritized converters and includes a fallback guess.
Can accepts consume input bytes?
It must leave the stream at its original position. The core checks that invariant before proceeding.
Sources
- Pinned sourceSource checked 2026-09-07
- Core dispatcherSource checked 2026-09-07
- Plain-text converterSource checked 2026-09-07
- Public exportsSource checked 2026-09-07