Pure Python library for converting Markdown to Slack's mrkdwn format. Zero dependencies, comprehensive formatting support, and proper handling of edge cases.
- Zero dependencies - Pure Python implementation with no external packages required
- Comprehensive formatting - Supports bold, italic, strikethrough, links, images, lists, and more
- Configurable - Customize symbols, formats, and enable/disable specific conversions
- Code block handling - Preserves content inside code blocks without conversion
- Table support - Wraps markdown tables in code blocks for Slack display
- Task lists - Converts checkbox syntax to Unicode symbols (☐/☑)
- Edge case handling - Properly handles nested formatting and special characters
from md2mrkdwn import convert
markdown = "**Hello** *World*! Check out [Slack](https://slack.com)"
mrkdwn = convert(markdown)
print(mrkdwn)
# Output: *Hello* _World_! Check out <https://slack.com|Slack># Install with pip
pip install md2mrkdwn
# Or install with uv
uv add md2mrkdwn
# Or install with pipx (for CLI tools that use this library)
pipx install md2mrkdwnThe convert() function provides a simple interface for one-off conversions:
from md2mrkdwn import convert
markdown = """
# Hello World
This is **bold** and *italic* text.
- Item 1
- Item 2
Check out [this link](https://example.com)!
"""
mrkdwn = convert(markdown)
print(mrkdwn)Output:
*Hello World*
This is *bold* and _italic_ text.
• Item 1
• Item 2
Check out <https://example.com|this link>!
For multiple conversions, use the MrkdwnConverter class:
from md2mrkdwn import MrkdwnConverter
converter = MrkdwnConverter()
# Convert multiple texts
text1 = converter.convert("**bold** and *italic*")
text2 = converter.convert("# Header\n\n- List item")
print(text1) # *bold* and _italic_
print(text2) # *Header*\n\n• List itemUse MrkdwnConfig to customize conversion behavior:
from md2mrkdwn import convert, MrkdwnConfig, MrkdwnConverter
# Custom bullet character
config = MrkdwnConfig(bullet_char="-")
print(convert("- Item 1\n- Item 2", config=config))
# Output: - Item 1
# - Item 2
# Custom checkbox symbols
config = MrkdwnConfig(checkbox_checked="✓", checkbox_unchecked="○")
print(convert("- [x] Done\n- [ ] Todo", config=config))
# Output: • ✓ Done
# • ○ Todo
# Plain headers (no bold)
config = MrkdwnConfig(header_style="plain")
print(convert("# Title", config=config))
# Output: Title
# URL-only links (no link text)
config = MrkdwnConfig(link_format="url_only")
print(convert("[Click here](https://example.com)", config=config))
# Output: <https://example.com>
# Disable specific conversions
config = MrkdwnConfig(convert_bold=False, convert_italic=False)
print(convert("**bold** and *italic*", config=config))
# Output: **bold** and *italic*
# Reusable converter with config
converter = MrkdwnConverter(MrkdwnConfig(
bullet_char="→",
horizontal_rule_char="=",
horizontal_rule_length=20
))
print(converter.convert("- Item\n\n---"))
# Output: → Item
#
# ====================| Option | Type | Default | Description |
|---|---|---|---|
bullet_char |
str | • |
Character for unordered list items |
checkbox_checked |
str | ☑ |
Symbol for checked task items |
checkbox_unchecked |
str | ☐ |
Symbol for unchecked task items |
horizontal_rule_char |
str | ─ |
Character for horizontal rules |
horizontal_rule_length |
int | 10 |
Length of horizontal rules |
header_style |
HeaderStyle | HeaderStyle.BOLD |
BOLD, PLAIN, or PREFIX |
link_format |
LinkFormat | LinkFormat.SLACK |
SLACK, URL_ONLY, or TEXT_ONLY |
table_mode |
TableMode | TableMode.CODE_BLOCK |
CODE_BLOCK or PRESERVE |
table_link_format |
LinkFormat | LinkFormat.URL_ONLY |
Link format inside tables |
strip_table_emoji |
bool | True |
Strip emoji shortcodes from tables |
convert_table_links |
bool | True |
Enable/disable link conversion in tables |
convert_bold |
bool | True |
Enable/disable bold conversion |
convert_italic |
bool | True |
Enable/disable italic conversion |
convert_strikethrough |
bool | True |
Enable/disable strikethrough conversion |
convert_links |
bool | True |
Enable/disable link conversion |
convert_images |
bool | True |
Enable/disable image conversion |
convert_lists |
bool | True |
Enable/disable list conversion |
convert_task_lists |
bool | True |
Enable/disable task list conversion |
convert_headers |
bool | True |
Enable/disable header conversion |
convert_horizontal_rules |
bool | True |
Enable/disable horizontal rule conversion |
convert_tables |
bool | True |
Enable/disable table wrapping |
Markdown tables are automatically wrapped in code blocks since Slack doesn't support native table rendering:
from md2mrkdwn import convert
markdown = """
| Name | Age |
|------|-----|
| Alice | 30 |
| Bob | 25 |
"""
print(convert(markdown))Output:
| Name | Age |
|---|---|
| Alice | 30 |
| Bob | 25 |
Links inside tables are converted to URL-only format by default (different from the global link_format setting):
from md2mrkdwn import convert, MrkdwnConfig, LinkFormat
markdown = """
| App | Link |
|-----|------|
| Example | [Visit](https://example.com) |
"""
# Default: URL only
print(convert(markdown))
# | App | Link |
# | Example | https://example.com |
# Slack format
config = MrkdwnConfig(table_link_format=LinkFormat.SLACK)
print(convert(markdown, config))
# | Example | <https://example.com|Visit> |
# Text only (link text, no URL)
config = MrkdwnConfig(table_link_format=LinkFormat.TEXT_ONLY)
print(convert(markdown, config))
# | Example | Visit || Markdown | mrkdwn | Notes |
|---|---|---|
**bold** or __bold__ |
*bold* |
Slack uses single asterisk |
*italic* or _italic_ |
_italic_ |
Slack uses underscores |
***bold+italic*** |
*_text_* |
Combined formatting |
~~strikethrough~~ |
~text~ |
Single tilde |
[text](url) |
<url|text> |
Slack link format |
 |
<url> |
Images become plain URLs |
# Header (all levels) |
*Header* |
Bold (Slack has no headers) |
- item / * item |
• item |
Bullet character (U+2022) |
1. item |
1. item |
Preserved as-is |
- [ ] task |
• ☐ task |
Unchecked checkbox (U+2610) |
- [x] task |
• ☑ task |
Checked checkbox (U+2611) |
> quote |
> quote |
Same syntax |
`code` |
`code` |
Same syntax |
code block |
code block |
Same syntax |
--- / *** |
────────── |
Horizontal rule (U+2500) |
| Tables | Wrapped in ``` | Slack has no native tables |
md2mrkdwn processes text through a multi-stage pipeline:
- Table extraction - Tables are detected, validated, and replaced with placeholders
- Code block tracking - Lines inside code blocks are skipped during conversion
- Pattern application - Regex patterns convert formatting using placeholder protection
- Placeholder restoration - Tables and temporary markers are replaced with final output
A key challenge in markdown conversion is preventing patterns from interfering with each other. For example, converting **bold** to *bold* could then be matched by the italic pattern.
md2mrkdwn solves this using placeholder substitution:
- Bold text is temporarily marked with null-byte placeholders
- Italic patterns run without matching the placeholders
- Placeholders are replaced with final mrkdwn characters
Tables are detected using these criteria:
- Lines matching
|...|pattern - Second row contains separator cells (dashes with optional alignment colons)
- Header and separator have matching column counts
Valid tables are wrapped in triple-backtick code blocks for monospace display in Slack.
Column alignment accounts for Unicode character display width. Emoji like ⭐ render as 2 columns wide in monospace fonts but have a character length of 1. The converter pads columns based on display width to maintain proper alignment.
Content inside code blocks (both fenced and inline) is protected from conversion:
- Fenced blocks: State machine tracks opening/closing ``` markers
- Inline code: Segments are extracted before conversion and restored after
git clone https://github.com/bigbag/md2mrkdwn.git
cd md2mrkdwn
make installmake install # Install all dependencies
make test # Run tests with coverage
make lint # Run linters (ruff + mypy)
make format # Format code with ruff
make clean # Clean cache and build files# Run all tests with coverage
uv run pytest --cov=md2mrkdwn --cov-report=term-missing
# Run specific test class
uv run pytest tests/test_converter.py::TestBasicFormatting -v
# Run with verbose output
uv run pytest -vmd2mrkdwn/
├── src/
│ └── md2mrkdwn/
│ ├── __init__.py # Package exports
│ └── converter.py # MrkdwnConverter, MrkdwnConfig classes
├── tests/
│ ├── conftest.py # Pytest fixtures
│ ├── test_converter.py # Converter tests
│ └── test_config.py # Configuration tests
├── pyproject.toml # Project configuration
├── Makefile # Development commands
└── README.md
Convert Markdown text to Slack mrkdwn format.
Parameters:
markdown- Input text in Markdown formatconfig- Optional configuration (uses defaults if not provided)
Returns:
- Text converted to Slack mrkdwn format
Class for converting Markdown to mrkdwn.
Constructor:
MrkdwnConverter(config: MrkdwnConfig | None = None)
Methods:
convert(markdown: str) -> str- Convert Markdown text to mrkdwn
Example:
converter = MrkdwnConverter()
result = converter.convert("**Hello** *World*")
# With custom config
config = MrkdwnConfig(bullet_char="-")
converter = MrkdwnConverter(config)
result = converter.convert("- Item")Immutable configuration dataclass for customizing conversion behavior.
Example:
from md2mrkdwn import MrkdwnConfig, DEFAULT_CONFIG
# Create custom config
config = MrkdwnConfig(
bullet_char="→",
header_style="plain",
convert_bold=False
)
# Use the default config singleton
print(DEFAULT_CONFIG.bullet_char) # •- Slack mrkdwn specification - Official Slack formatting documentation
- markdown_to_mrkdwn - Related project for markdown to mrkdwn conversion
MIT License - see LICENSE file.