Skip to main content
Pre-release

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> paths
  • keep_comments - preserve HTML comments in output (default: True)
  • custom_components - list of custom component classes to register
  • validation_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), or strict

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

mjml-1.0.0a1.tar.gz (176.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mjml-1.0.0a1-py3-none-any.whl (88.9 kB view details)

Uploaded Python 3

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

Hashes for mjml-1.0.0a1.tar.gz
Algorithm Hash digest
SHA256 966d72121589cbfb01d2e4de16e12dc8ba9364780e3aedf08bfbf8fd6e8a5261
MD5 dfae790c2f514d73ba12afa4ff7f1583
BLAKE2b-256 6e580c4dde64c5f07d3e30774145d9b3a286a8c83a7955207a39e107c369dc63

See more details on using hashes here.

Provenance

The following attestation bundles were made for mjml-1.0.0a1.tar.gz:

Publisher: release.yml on FelixSchwarz/mjml-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for mjml-1.0.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 7586ac7bab16cf25f82e58c130343289b652da2fc842c2c11b0a764191faebb2
MD5 d7906bbadc20392cca2328e3f0167f85
BLAKE2b-256 fef94e8650233b48921c27f8ee695b743a8626f2b192ad646995d73aaa1282fb

See more details on using hashes here.

Provenance

The following attestation bundles were made for mjml-1.0.0a1-py3-none-any.whl:

Publisher: release.yml on FelixSchwarz/mjml-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.0.0a1 This release

2 files

0.12.0

2 files

0.11.1

2 files

0.11.0

2 files

0.10.0

2 files

0.9.1

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.2

2 files

0.6.1

2 files

0.6.0

2 files

0.5.4

2 files

0.5.3

2 files

0.5.2

2 files

0.5.1

2 files

0.5

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page