How to Publish a Python Package to conda-forge
Your package is on PyPI, so pip install and uv add work. Scientific and machine-learning users reach for conda install instead, and that pulls from conda-forge, the community channel that carries over 25,000 packages.
Getting onto conda-forge means submitting a build recipe once. After it merges, conda-forge’s bots create a feedstock repository for your package and keep it updated as you publish new PyPI releases. This guide walks through that first submission.
Prerequisites
- A package already published to PyPI (see Publishing Your First Python Package to PyPI). The release must include a source distribution, not only wheels.
- A GitHub account.
- uv or Pixi installed, to install the recipe generator in the next step.
Install grayskull
grayskull reads a package’s PyPI metadata and writes a conda recipe. Install it as a standalone command-line tool:
uv tool install grayskullBoth put a grayskull binary on your PATH. Confirm it runs:
$ grayskull --version
3.1.1
Fork and clone staged-recipes
Every new package enters conda-forge through one repository, conda-forge/staged-recipes. Fork it, clone your fork, and create a branch named after your package:
gh repo fork conda-forge/staged-recipes --clone
cd staged-recipes
git checkout -b humanizeIf you don’t use the GitHub CLI, fork the repository in the web UI, then git clone your fork manually.
Generate the recipe
From the recipes/ directory, point grayskull at your package’s name on PyPI:
cd recipes
grayskull pypi --strict-conda-forge humanizegrayskull downloads the sdist, reads its metadata, and writes recipes/humanize/meta.yaml. The --strict-conda-forge flag drops platform selectors that conda-forge doesn’t need. The result for a small pure-Python package looks like this:
{% set python_min = "3.10" %}
{% set version = "4.15.0" %}
package:
name: humanize
version: {{ version }}
source:
url: https://pypi.org/packages/source/h/humanize/humanize-{{ version }}.tar.gz
sha256: 1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10
build:
noarch: python
script: {{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
number: 0
requirements:
host:
- python {{ python_min }}.*
- hatch-vcs
- hatchling >=1.27
- pip
run:
- python >={{ python_min }}
test:
imports:
- humanize
commands:
- pip check
requires:
- pip
- python {{ python_min }}.*
about:
home: https://github.com/python-humanize/humanize
summary: Python humanize utilities
license: MIT
license_file: PLEASE_ADD_LICENSE_FILE
extra:
recipe-maintainers:
- AddYourGitHubIdHereIf grayskull stops with There is no sdist package on pypi for <name>, your release shipped only wheels. conda-forge builds from source, so run uv build to produce an sdist, upload it to PyPI, and try again.
Note
You might see two recipe formats mentioned online: the older meta.yaml (shown here) and a newer recipe.yaml. Both build the same package. Use meta.yaml; it’s what grayskull writes by default and what nearly every conda-forge package uses.
Fill in what grayskull leaves blank
grayskull gets most fields right but leaves two that fail CI if you ship them as-is:
license_file: grayskull writesPLEASE_ADD_LICENSE_FILEwhen it can’t locate the license inside the sdist. Replace it with the license file’s name as packaged, for exampleLICENSEorLICENSE.txt. The recipe linter rejects the missing value.recipe-maintainers: grayskull fills in the GitHub username it detects from your environment, or the literal placeholderAddYourGitHubIdHerewhen it finds none. Set it to your GitHub handle and add any co-maintainers.
The sha256 line is computed from the sdist grayskull downloaded. If the hash later stops matching (PyPI returns a fresh checksum when a maintainer re-uploads a release), re-run grayskull to regenerate it rather than editing the digit string by hand.
Open the pull request
Commit the recipe and push it to your fork:
git add recipes/humanize/meta.yaml
git commit -m "Add humanize recipe"
git push -u origin humanizeOpen a pull request against conda-forge/staged-recipes. The PR template includes a checklist covering the fields you just edited.
Get the recipe reviewed and merged
Opening the PR triggers conda-forge’s CI, which builds your package on Linux, macOS, and Windows and runs a recipe linter. For a noarch: python package, only the Linux build must pass; the macOS and Windows builds are expected to skip. The test: section grayskull generated imports your package and runs pip check inside the freshly built environment.
When the checks are green, request a reviewer by commenting on the PR:
@conda-forge-admin, please ping teamIf pip check fails with <package> requires <dep> which is not installed, a runtime dependency is missing from the run: list. Add it, push again, and the check reruns. Reviewers are volunteers, so expect a few days for a response.
Inherit your new feedstock
When a maintainer merges the PR, conda-forge’s bots create a dedicated repository, humanize-feedstock, and add you as a maintainer. The package builds within an hour or two and appears on the channel:
conda install -c conda-forge humanize # or: pixi add humanizeFrom this point the feedstock, not staged-recipes, holds the recipe. Edits go to the feedstock repository.
Let the bots ship future releases
You don’t repeat this process for every release. regro-cf-autotick-bot watches PyPI and, within hours of a new version, opens a pull request on your feedstock that bumps the version and sha256. Review it, wait for CI, and merge. When a release adds or drops a dependency, edit the requirements: section in that same PR before merging.
Learn More
- Contributing packages documents the full staged-recipes workflow
- grayskull on GitHub lists every recipe-generation flag
- conda-forge explains the channel and its build infrastructure
- uv vs. Pixi vs. Conda for scientific Python compares the tools that install from conda-forge