mdx_better_lists
A Python-Markdown extension for better list handling, providing more intuitive list behavior and formatting with fine-grained control over list rendering. Created with Test-Driven Development (TDD) principles to ensure reliability and maintainability.
Features
- Configurable nested indentation - Control how many spaces are required for nested lists (default: 2)
- Marker-based list separation - Automatically separate lists when marker types change (-, *, +)
- Blank line list separation - Separate unordered lists when blank lines appear between items
- Loose list control - Control paragraph wrapping in ordered lists with blank lines
- Number preservation - Optionally preserve exact list numbers from markdown source
- Always start at one - Force ordered lists to always start at 1
- Paragraph-list splitting - Optionally split paragraphs and lists without requiring blank lines between them
Installation
pip install mdx_better_lists
Usage
Basic Usage
from markdown import markdown
text = """
- Item 1
- Item 2
- Item 3
"""
html = markdown(text, extensions=['mdx_better_lists'])
With Configuration
from markdown import markdown
text = """
1. First
2. Second
2. Another second
"""
html = markdown(text, extensions=['mdx_better_lists'],
extension_configs={'mdx_better_lists': {
'preserve_numbers': True
}})
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
nested_indent |
int | 2 | Number of spaces required for nested list indentation |
marker_separation |
bool | True | Separate lists when marker types (-, *, +) differ |
unordered_list_separation |
bool | True | Separate unordered lists when blank lines appear between items |
ordered_list_loose |
bool | True | Wrap ordered list items in <p> tags when blank lines separate them |
preserve_numbers |
bool | False | Preserve exact list numbers from markdown (use value attribute) |
always_start_at_one |
bool | False | Force all ordered lists to start at 1 |
split_paragraph_lists |
bool | False | Split paragraphs and lists when they appear without blank lines between them |
Configuration Details
nested_indent (default: 2)
Controls how many spaces are required for a list item to be considered nested.
# With nested_indent=2 (default)
- Parent
- Nested # 2 spaces = nested
# With nested_indent=4
- Parent
- Nested # 4 spaces = nested
- Not nested # 2 spaces = not nested
marker_separation (default: True)
When enabled, lists with different markers (-, *, +) are separated into different <ul> elements.
# With marker_separation=True (default)
- Item with dash
+ Item with plus # Creates a new <ul>
# Output: Two separate <ul> elements
# With marker_separation=False
- Item with dash
+ Item with plus # Same <ul>
# Output: Single <ul> element
unordered_list_separation (default: True)
When enabled, unordered lists are separated into different <ul> elements when blank lines appear.
# With unordered_list_separation=True (default)
- First
- Second # Creates a new <ul>
# Output: Two separate <ul> elements
# With unordered_list_separation=False
- First
- Second # Same <ul>
# Output: Single <ul> element
ordered_list_loose (default: True)
When enabled, ordered list items are wrapped in <p> tags when blank lines separate them.
# With ordered_list_loose=True (default)
1. First
2. Second
# Output:
<ol>
<li><p>First</p></li>
<li><p>Second</p></li>
</ol>
# With ordered_list_loose=False
1. First
2. Second
# Output:
<ol>
<li>First</li>
<li>Second</li>
</ol>
preserve_numbers (default: False)
When enabled, preserves exact list numbers from the markdown source using the value attribute.
# With preserve_numbers=True
1. First
2. Second
2. Another second
3. Third
# Output:
<ol>
<li value="1">First</li>
<li value="2">Second</li>
<li value="2">Another second</li>
<li value="3">Third</li>
</ol>
always_start_at_one (default: False)
When enabled, forces all ordered lists to start at 1, ignoring the starting number in markdown.
# With always_start_at_one=True
5. Fifth
6. Sixth
# Output:
<ol>
<li>Fifth</li>
<li>Sixth</li>
</ol>
# (renders as 1, 2 instead of 5, 6)
# With always_start_at_one=False (default)
5. Fifth
6. Sixth
# Output:
<ol start="5">
<li>Fifth</li>
<li>Sixth</li>
</ol>
split_paragraph_lists (default: False)
When enabled, automatically splits paragraphs and lists that appear without blank lines between them into separate blocks. This allows lists to be recognized immediately after paragraphs without requiring a blank line separator.
# With split_paragraph_lists=False (default)
This is a paragraph before the list.
- First item
- Second item
# Output:
<p>This is a paragraph before the list.
- First item
- Second item</p>
# (list markers are treated as plain text)
# With split_paragraph_lists=True
This is a paragraph before the list.
- First item
- Second item
# Output:
<p>This is a paragraph before the list.</p>
<ul>
<li>First item</li>
<li>Second item</li>
</ul>
# (paragraph and list are separated)
This also works with ordered lists:
# With split_paragraph_lists=True
Introduction paragraph.
1. First point
2. Second point
# Output:
<p>Introduction paragraph.</p>
<ol>
<li>First point</li>
<li>Second point</li>
</ol>
Note: This feature only operates at the top level. List markers inside list items that are not properly indented will remain as text (standard Markdown behavior).
Examples
Example 1: Marker Separation
from markdown import markdown
text = """
- Item with dash
- Another dash
+ Item with plus
+ Another plus
"""
html = markdown(text, extensions=['mdx_better_lists'])
# Output: Three separate <ul> elements (blank line + marker change)
Example 2: Nested Lists with Custom Indentation
from markdown import markdown
text = """
- Parent
- Nested (4 spaces)
- Deeply nested (8 spaces)
"""
html = markdown(text, extensions=['mdx_better_lists'],
extension_configs={'mdx_better_lists': {
'nested_indent': 4
}})
Example 3: Preserving List Numbers
from markdown import markdown
text = """
1. Introduction
1. Background
1. Methods
"""
html = markdown(text, extensions=['mdx_better_lists'],
extension_configs={'mdx_better_lists': {
'preserve_numbers': True
}})
# Each item gets value="1"
Migration from mdx_truly_sane_lists
# mdx_truly_sane_lists
markdown(text, extensions=['mdx_truly_sane_lists'],
extension_configs={'mdx_truly_sane_lists': {
'truly_sane': True # Default
'nested_indent': 2 # Default
}})
# Equivalent in mdx_better_lists (this is the default)
markdown(text, extensions=['mdx_better_lists'],
extension_configs={'mdx_better_lists': {
'marker_separation': True, # Default
'unordered_list_separation': True, # Default
'ordered_list_loose': True # Default
'nested_indent': 2 # Default
}})
Note: mdx_better_lists does not support loose list behavior (paragraph wrapping) for unordered lists. Unordered lists always remain tight, even when both marker_separation and unordered_list_separation are set to False.
Development
This project follows Test-Driven Development (TDD) principles.
Running Tests
pytest tests/
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
- Inspired by mdx_truly_sane_lists
- Built on Python-Markdown
Metadata
Release files for mdx-better-lists 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mdx_better_lists-1.1.0.tar.gz | 23.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mdx_better_lists-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 32.1 kB
Release files / mdx_better_lists-1.1.0.tar.gz
| Download URL | mdx_better_lists-1.1.0.tar.gz |
|---|---|
| Size | 23.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
02ca2999a5f558bbd56c392b86381cf993497870cd7f11320c990907d1cc3730
|
|
BLAKE2b-256 checksum How to use checksums |
0af50b8880572f39e5b2e4fc2a7c1a7adcf70e81fbaabdb35943ed488bc09f71
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Feb 18, 2026.
Transparency logRelease files / mdx_better_lists-1.1.0-py3-none-any.whl
| Download URL | mdx_better_lists-1.1.0-py3-none-any.whl |
|---|---|
| Size | 8.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
606f9dab04a1b2fb1ccb525753a423c30b17024e17f49e5314e81504f891d9f6
|
|
BLAKE2b-256 checksum How to use checksums |
c70dfcd2020b3d518d28c011677aae7b8065070a385b6f908507a2bf599b68d8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Feb 18, 2026.
Transparency log