Run twine check dist/* on your built files. It reports the same rendering problems PyPI would, usually invalid reStructuredText, an unsupported markup type, or a wrong content type. Fix the README, rebuild, and check again before uploading, because PyPI rejects uploads whose description fails to render.
Why PyPI is strict
The PyPI help page states that PyPI will reject uploads if the package description fails to render. The page you ship is your project's front door, and a broken one is a poor first impression, so the check happens at upload time.
Supported formats
The packaging guide lists what PyPI's README renderer supports.
| Format | How to declare it |
|---|---|
| Plain text | readme pointing to a text file |
| reStructuredText without Sphinx extensions | File ending in .rst |
| Markdown, GitHub flavored by default, or CommonMark | File ending in .md |
When you use pyproject.toml, the extension of the readme value controls how PyPI renders the description. A readme of README.md renders as Markdown, and README.rst renders as reStructuredText. You can also set the content type explicitly with a table form.
[project]
readme = { file = "README.txt", content-type = "text/markdown" }
Common causes and fixes
- Sphinx only markup in reStructuredText. The guide says Sphinx directives and roles, such as py:func or ref roles used in docstrings, are not allowed and produce errors like an unknown interpreted text role. Replace them with plain links or plain text.
- Invalid reStructuredText. The guide notes that invalid markup prevents rendering, so PyPI shows the raw source or rejects the file. Fix indentation, heading underline lengths and list formatting.
- Wrong content type. If a Markdown file is declared as another type, rendering breaks. Match the declared type to the file.
- Old tooling. The guide lists minimum tool versions for GitHub flavored Markdown support in setuptools, wheel and twine. Upgrade them.
- README path not included. If the path in pyproject.toml is wrong, the build fails earlier. Check the path relative to pyproject.toml.
Check before you upload
- Build with
python -m build. - Run
twine check dist/*. - Read the output. A good run says Passed for each file.
- If it fails, fix the README and rebuild. Do not edit the built archive by hand.
The guide says twine check requires twine 1.12.0 or higher.
Add it to CI
Put the check in your release workflow, so a broken README stops the job before the upload step. This also keeps your TestPyPI rehearsal honest. See how to test a release on TestPyPI.
What belongs in a library README
A short summary of what the library does, an install command, a minimal example, a link to documentation, your security contact and a license note. Keep relative image links out, because they will not resolve on PyPI unless you use full URLs.
Safeguard can run policy checks in your pipeline next to this one. A free scan is a quick way to see what Safeguard reports for your project.
Quick check
Does PyPI accept Markdown READMEs?
Yes. It renders GitHub flavored Markdown by default, or CommonMark if you choose it.
Why do my docstring roles break the README?
PyPI renders reStructuredText without Sphinx extensions, so Sphinx directives and roles are errors there.
How do I check locally?
Build your distributions, then run twine check on the files in dist.
Sources
- Making a PyPI-friendly README, Python Packaging User Guide
- PyPI help, FAQ
- Writing your pyproject.toml, Python Packaging User Guide
Last reviewed: 19 October 2026.