This release is a pre-release and may not be stable for production use.
mjml-python
A pure Python implementation of MJML v5 (unreleased, "main" branch), the email markup language created by Mailjet. Build responsive HTML emails without requiring JavaScript, Node.js or Rust. For MJML v4, use the 0.x release series.
All standard MJML components are supported, and the rendered output closely follows the upstream JavaScript implementation.
Installation
pip install mjml
For optional CSS inlining support:
pip install mjml[css_inlining]
Usage
Python API
from mjml import mjml_to_html
# From a file
with open('my_email.mjml', 'rb') as fp:
result = mjml_to_html(fp)
# From a string
result = mjml_to_html('<mjml><mj-body>...</mj-body></mjml>')
assert not result.errors
html: str = result.html
The mjml_to_html() function accepts several optional parameters:
template_dir- base directory for resolving<mj-include>pathskeep_comments- preserve HTML comments in output (default:True)custom_components- list of custom component classes to registervalidation_level-'skip','soft'(default), or'strict', see Validation
CLI
# Convert and print to stdout
$ mjml my_email.mjml
# Convert and write to file
$ mjml my_email.mjml -o output.html
# Read from stdin
$ cat my_email.mjml | mjml -
CLI options:
--template-dir=<path>- base directory for<mj-include>(default: directory of the input file)--config.keepComments=False- strip HTML comments from output--validate- report problems in the template, generate no HTML and exit nonzero when something was found--validation-level=<level>-skip,soft(default), orstrict
Validation
Validation checks an MJML template either on its own or before generating HTML. It reports unknown or misplaced elements, unsupported attributes, invalid attribute values, unreadable includes, and MJML JS features which this Python port cannot reproduce correctly.
Validation levels
mjml_to_html() supports three validation levels:
| Level | Behavior |
|---|---|
skip |
Generate HTML without validating the template. |
soft (default) |
Validate and generate HTML. Problems are returned in result.errors. |
strict |
Validate first. Generate HTML only when no errors were found; otherwise raise MJMLValidationErrors. |
Command line
Use --validation-level to validate while converting a template:
# Report problems (on stderr) but generate HTML anyway if at all possible
# Refuse to generate HTML when validation fails
mjml --validation-level=strict my_email.mjml
To validate without generating HTML, use:
mjml --validate my_email.mjml
--validate writes problems to standard error and exits with a nonzero status
when any were found.
Python API
Soft validation lets an application report problems without preventing HTML generation:
from mjml import mjml_to_html
result = mjml_to_html(mjml_input, validation_level='soft')
for error in result.errors:
print(error.formatted_message())
html = result.html
Strict validation prevents generation when the template contains errors:
from mjml import MJMLValidationErrors, mjml_to_html
try:
result = mjml_to_html(mjml_input, validation_level='strict')
except MJMLValidationErrors as error:
for validation_error in error.errors:
print(validation_error.formatted_message())
Validation without rendering
Use validate() when only the validation result is needed:
from mjml import validate
errors = validate(mjml_input)
for error in errors:
print(error.formatted_message())
Each ValidationError provides a message, the affected element, its source
location when available, and the rule which reported it. Included templates
also retain information about the chain of files through which they were
included. formatted_message() combines this information into a
human-readable line.
Supported Components
All standard MJML v4 components are implemented. The project comes with no guarantee that additions or changes to the standard are implemented, or in which timing -- but coverage of the standard is a principal objective of the project.
Layout: mj-body, mj-section, mj-column, mj-group, mj-wrapper, mj-hero
Content: mj-text, mj-image, mj-button, mj-table, mj-divider, mj-spacer, mj-raw
Interactive: mj-accordion, mj-carousel, mj-navbar, mj-social
Head: mj-head, mj-title, mj-preview, mj-style, mj-attributes, mj-breakpoint, mj-font, mj-html-attributes
Other: mj-include (file includes with relative/absolute paths)
Custom Components
You can register your own components:
from mjml.core.api import ComponentCategory
from mjml.elements import BodyComponent
class MyComponent(BodyComponent):
component_name = 'mj-my-component'
categories = frozenset({ComponentCategory.BODY_ELEMENT})
@classmethod
def allowed_attrs(cls):
# the element it is placed in reads these from every child
return {
'padding' : 'unit(px,%){1,4}',
'padding-top' : 'unit(px,%)',
'padding-right' : 'unit(px,%)',
'padding-bottom': 'unit(px,%)',
'padding-left' : 'unit(px,%)',
}
def render(self):
return '<p>whatever this component renders</p>'
result = mjml_to_html(mjml_input, custom_components=[MyComponent])
categories says where the component may be used and is what
registerDependencies() declares in the JavaScript implementation. A component
which declares none is reported as misplaced wherever it is put, exactly as
mjml js rejects a custom component which registered no dependencies. A subclass
of a built-in component inherits the categories of the element it derives from.
Limitations
Compared to the JavaScript MJML implementation, the following features are not available:
- Minification of the generated HTML
- Beautification (pretty-printing) of the generated HTML
If you need these features, see the Alternatives section below.
Goals / Motivation
This library tracks the JavaScript version of mjml so you should get the same HTML output for supported components. There may be minor differences due to the manual porting process.
Why a Python port?
- No Node.js dependency: avoid deploying a Node.js stack and auditing hundreds of npm packages
- Data privacy: no need for third-party API services
- Fast startup: CPython converts a trivial template in ~70ms vs ~650ms for Node.js, making it practical for CLI use and on-demand email generation
- Tight integration: embed directly in Python web applications, Django/Flask views, or background workers
Alternatives / Additional Resources
- django-mjml: integrates the JavaScript mjml with Django templates (github). Requires Node.js but gives access to all upstream features.
- MJML.NET: unofficial C# port of mjml (github)
- mrml: Rust implementation of mjml (github)
- email-bugs: knowledge base about rendering quirks in email clients
- htmlemailcheck: commercial email rendering checker with a free knowledge base
- #emailgeeks: Slack community for email developers and designers
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mjml-1.0.0a1.tar.gz.
File metadata
- Download URL: mjml-1.0.0a1.tar.gz
- Upload date:
- Size: 176.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
966d72121589cbfb01d2e4de16e12dc8ba9364780e3aedf08bfbf8fd6e8a5261
|
|
| MD5 |
dfae790c2f514d73ba12afa4ff7f1583
|
|
| BLAKE2b-256 |
6e580c4dde64c5f07d3e30774145d9b3a286a8c83a7955207a39e107c369dc63
|
Provenance
The following attestation bundles were made for mjml-1.0.0a1.tar.gz:
Publisher:
release.yml on FelixSchwarz/mjml-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mjml-1.0.0a1.tar.gz -
Subject digest:
966d72121589cbfb01d2e4de16e12dc8ba9364780e3aedf08bfbf8fd6e8a5261 - Sigstore transparency entry: 2804247310
- Sigstore integration time:
-
Permalink:
FelixSchwarz/mjml-python@46b593fa788bc6cbefede19e25e67354ca91a163 -
Branch / Tag:
refs/tags/v1.0.0a1 - Owner: https://github.com/FelixSchwarz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@46b593fa788bc6cbefede19e25e67354ca91a163 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mjml-1.0.0a1-py3-none-any.whl.
File metadata
- Download URL: mjml-1.0.0a1-py3-none-any.whl
- Upload date:
- Size: 88.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7586ac7bab16cf25f82e58c130343289b652da2fc842c2c11b0a764191faebb2
|
|
| MD5 |
d7906bbadc20392cca2328e3f0167f85
|
|
| BLAKE2b-256 |
fef94e8650233b48921c27f8ee695b743a8626f2b192ad646995d73aaa1282fb
|
Provenance
The following attestation bundles were made for mjml-1.0.0a1-py3-none-any.whl:
Publisher:
release.yml on FelixSchwarz/mjml-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mjml-1.0.0a1-py3-none-any.whl -
Subject digest:
7586ac7bab16cf25f82e58c130343289b652da2fc842c2c11b0a764191faebb2 - Sigstore transparency entry: 2804247324
- Sigstore integration time:
-
Permalink:
FelixSchwarz/mjml-python@46b593fa788bc6cbefede19e25e67354ca91a163 -
Branch / Tag:
refs/tags/v1.0.0a1 - Owner: https://github.com/FelixSchwarz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@46b593fa788bc6cbefede19e25e67354ca91a163 -
Trigger Event:
push
-
Statement type: