Skip to main content
Plugins allow you to package and distribute custom converters as standalone Python packages. MarkItDown automatically discovers and loads plugins using Python’s entry points system.

Plugin Architecture

MarkItDown uses the markitdown.plugin entry point group to discover plugins. When enable_plugins=True, MarkItDown calls each plugin’s register_converters() function during initialization.
Plugins are disabled by default. Users must explicitly enable them with enable_plugins=True or the --use-plugins CLI flag.

Creating a Plugin

1
Set Up the Package Structure
2
Create a standard Python package structure:
3
markitdown-sample-plugin/
├── src/
│   └── markitdown_sample_plugin/
│       ├── __init__.py
│       ├── __about__.py
│       └── _plugin.py
├── tests/
│   ├── __init__.py
│   ├── test_sample_plugin.py
│   └── test_files/
│       └── test.rtf
├── pyproject.toml
└── README.md
4
Implement the Plugin Interface
5
In your main plugin module (e.g., _plugin.py), implement the required interface:
6
from markitdown import (
    MarkItDown,
    DocumentConverter,
    DocumentConverterResult,
    StreamInfo,
)
from typing import BinaryIO, Any

# REQUIRED: Plugin interface version
__plugin_interface_version__ = 1

# REQUIRED: Registration function
def register_converters(markitdown: MarkItDown, **kwargs):
    """
    Called during MarkItDown construction to register converters.
    
    Parameters:
    - markitdown: The MarkItDown instance to register converters with
    - **kwargs: Additional configuration passed from MarkItDown constructor
    """
    markitdown.register_converter(RtfConverter())
7
Implement Your Converter
8
Create your custom converter class:
9
class RtfConverter(DocumentConverter):
    """Converts RTF files to Markdown."""
    
    def accepts(
        self,
        file_stream: BinaryIO,
        stream_info: StreamInfo,
        **kwargs: Any,
    ) -> bool:
        mimetype = (stream_info.mimetype or "").lower()
        extension = (stream_info.extension or "").lower()
        
        if extension in [".rtf"]:
            return True
        
        if mimetype.startswith("text/rtf") or mimetype.startswith("application/rtf"):
            return True
        
        return False
    
    def convert(
        self,
        file_stream: BinaryIO,
        stream_info: StreamInfo,
        **kwargs: Any,
    ) -> DocumentConverterResult:
        from striprtf.striprtf import rtf_to_text
        import locale
        
        # Decode the file
        encoding = stream_info.charset or locale.getpreferredencoding()
        stream_data = file_stream.read().decode(encoding)
        
        # Convert to plain text
        markdown = rtf_to_text(stream_data)
        
        return DocumentConverterResult(
            markdown=markdown,
            title=None
        )
10
Export the Interface
11
In your __init__.py, export the required symbols:
12
from ._plugin import (
    __plugin_interface_version__,
    register_converters,
    RtfConverter,
)

__all__ = [
    "__plugin_interface_version__",
    "register_converters",
    "RtfConverter",
]
13
Configure pyproject.toml
14
Add the entry point to your pyproject.toml:
15
[project]
name = "markitdown-sample-plugin"
dynamic = ["version"]
description = "A sample plugin for the markitdown library."
requires-python = ">=3.10"
dependencies = [
    "markitdown>=0.1.0a1",
    "striprtf",
]

# CRITICAL: This entry point enables plugin discovery
[project.entry-points."markitdown.plugin"]
sample_plugin = "markitdown_sample_plugin"
16
The entry point format is:
17
[project.entry-points."markitdown.plugin"]
<plugin_name> = "<package_name>"
18
  • <plugin_name>: A unique identifier for your plugin
  • <package_name>: The fully qualified name of your package
  • Complete Example: RTF Plugin

    Here’s the complete RTF converter plugin from packages/markitdown-sample-plugin/:
    import locale
    from typing import BinaryIO, Any
    from striprtf.striprtf import rtf_to_text
    
    from markitdown import (
        MarkItDown,
        DocumentConverter,
        DocumentConverterResult,
        StreamInfo,
    )
    
    __plugin_interface_version__ = 1
    
    ACCEPTED_MIME_TYPE_PREFIXES = [
        "text/rtf",
        "application/rtf",
    ]
    
    ACCEPTED_FILE_EXTENSIONS = [".rtf"]
    
    def register_converters(markitdown: MarkItDown, **kwargs):
        """
        Called during construction of MarkItDown instances.
        """
        markitdown.register_converter(RtfConverter())
    
    class RtfConverter(DocumentConverter):
        """Converts RTF files to Markdown."""
        
        def accepts(
            self,
            file_stream: BinaryIO,
            stream_info: StreamInfo,
            **kwargs: Any,
        ) -> bool:
            mimetype = (stream_info.mimetype or "").lower()
            extension = (stream_info.extension or "").lower()
            
            if extension in ACCEPTED_FILE_EXTENSIONS:
                return True
            
            for prefix in ACCEPTED_MIME_TYPE_PREFIXES:
                if mimetype.startswith(prefix):
                    return True
            
            return False
        
        def convert(
            self,
            file_stream: BinaryIO,
            stream_info: StreamInfo,
            **kwargs: Any,
        ) -> DocumentConverterResult:
            # Read the file using the charset or system default
            encoding = stream_info.charset or locale.getpreferredencoding()
            stream_data = file_stream.read().decode(encoding)
            
            # Convert and return
            return DocumentConverterResult(
                title=None,
                markdown=rtf_to_text(stream_data),
            )
    

    Installing and Testing Your Plugin

    1
    Install in Development Mode
    2
    cd markitdown-sample-plugin
    pip install -e .
    
    3
    Verify Plugin Discovery
    4
    Check that MarkItDown can find your plugin:
    5
    markitdown --list-plugins
    
    6
    You should see your plugin listed in the output.
    7
    Test with CLI
    8
    Use the --use-plugins flag to enable plugins:
    9
    markitdown --use-plugins path-to-file.rtf
    
    10
    Test with Python
    11
    Enable plugins when creating the MarkItDown instance:
    12
    from markitdown import MarkItDown
    
    md = MarkItDown(enable_plugins=True)
    result = md.convert("path-to-file.rtf")
    print(result.markdown)
    

    Writing Plugin Tests

    Create tests to verify both direct converter usage and plugin loading:
    import os
    from markitdown import MarkItDown, StreamInfo
    from markitdown_sample_plugin import RtfConverter
    
    TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "test_files")
    
    def test_converter():
        """Test the converter directly."""
        with open(os.path.join(TEST_FILES_DIR, "test.rtf"), "rb") as file_stream:
            converter = RtfConverter()
            result = converter.convert(
                file_stream=file_stream,
                stream_info=StreamInfo(
                    mimetype="text/rtf",
                    extension=".rtf",
                    filename="test.rtf"
                ),
            )
            
            assert "Expected content" in result.markdown
    
    def test_markitdown():
        """Test that MarkItDown loads the plugin correctly."""
        md = MarkItDown(enable_plugins=True)
        result = md.convert(os.path.join(TEST_FILES_DIR, "test.rtf"))
        
        assert "Expected content" in result.markdown
    

    Plugin Loading Process

    MarkItDown loads plugins using this process (_markitdown.py:65):
    1. When enable_plugins=True, MarkItDown calls enable_plugins() method
    2. _load_plugins() discovers all markitdown.plugin entry points
    3. Each entry point is loaded and its register_converters() function is called
    4. If any plugin fails to load, a warning is issued and the plugin is skipped
    def _load_plugins() -> List[Any]:
        """Lazy load plugins."""
        plugins = []
        for entry_point in entry_points(group="markitdown.plugin"):
            try:
                plugins.append(entry_point.load())
            except Exception:
                warn(f"Plugin '{entry_point.name}' failed to load")
        return plugins
    

    Best Practices

    Version Compatibility: Always set __plugin_interface_version__ = 1 and specify minimum MarkItDown version in dependencies.
    Graceful Degradation: Handle import errors for optional dependencies and provide helpful error messages.
    Priority Configuration: Allow users to pass priority via **kwargs in register_converters():
    def register_converters(markitdown: MarkItDown, **kwargs):
        priority = kwargs.get("my_converter_priority", 0.0)
        markitdown.register_converter(MyConverter(), priority=priority)
    
    Documentation: Include clear README with installation instructions and example usage.

    Publishing Your Plugin

    To share your plugin with others:
    1. Naming Convention: Use markitdown-<name>-plugin for package name
    2. PyPI Publishing: Follow standard Python package publishing process
    3. Documentation: Include clear installation and usage instructions
    4. Testing: Ensure comprehensive test coverage
    # Build the package
    python -m build
    
    # Upload to PyPI
    python -m twine upload dist/*
    
    Users can then install your plugin:
    pip install markitdown-sample-plugin
    

    Next Steps

    Custom Converters

    Learn more about implementing converters

    Configuration

    Understand MarkItDown configuration options

    Build docs developers (and LLMs) love