# How to Publish a Python Package to conda-forge


Your package is on [PyPI](https://pydevtools.com/handbook/explanation/what-is-pypi.md), so `pip install` and `uv add` work. Scientific and machine-learning users reach for `conda install` instead, and that pulls from [conda-forge](https://pydevtools.com/handbook/reference/conda-forge.md), 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](https://pypi.org) (see [Publishing Your First Python Package to PyPI](https://pydevtools.com/handbook/tutorial/publishing-your-first-python-package-to-pypi.md)). The release must include a [source distribution](https://pydevtools.com/handbook/reference/sdist.md), not only [wheels](https://pydevtools.com/handbook/reference/wheel.md).
* A GitHub account.
* [uv](https://pydevtools.com/handbook/reference/uv.md) or [Pixi](https://pydevtools.com/handbook/reference/pixi.md) installed, to install the recipe generator in the next step.

## Install grayskull

[grayskull](https://github.com/conda/grayskull) reads a package's PyPI metadata and writes a conda recipe. Install it as a standalone command-line tool:

```bash
uv tool install grayskull
```
```bash
pixi global install grayskull
```
Both put a `grayskull` binary on your `PATH`. Confirm it runs:

```console
$ grayskull --version
3.1.1
```

## Fork and clone staged-recipes

Every new package enters conda-forge through one repository, [conda-forge/staged-recipes](https://github.com/conda-forge/staged-recipes). Fork it, clone your fork, and create a branch named after your package:

```bash
gh repo fork conda-forge/staged-recipes --clone
cd staged-recipes
git checkout -b humanize
```

If you don't use the [GitHub CLI](https://cli.github.com/), 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:

```bash
cd recipes
grayskull pypi --strict-conda-forge humanize
```

grayskull 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:

```yaml {filename="recipes/humanize/meta.yaml"}
{% 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:
    - AddYourGitHubIdHere
```

If 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 writes `PLEASE_ADD_LICENSE_FILE` when it can't locate the license inside the sdist. Replace it with the license file's name as packaged, for example `LICENSE` or `LICENSE.txt`. The recipe linter rejects the missing value.
* **`recipe-maintainers`**: grayskull fills in the GitHub username it detects from your environment, or the literal placeholder `AddYourGitHubIdHere` when 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:

```bash
git add recipes/humanize/meta.yaml
git commit -m "Add humanize recipe"
git push -u origin humanize
```

Open 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:

```text
@conda-forge-admin, please ping team
```

If `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:

```bash
conda install -c conda-forge humanize   # or: pixi add humanize
```

From 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](https://conda-forge.org/docs/maintainer/adding_pkgs/) documents the full staged-recipes workflow
* [grayskull on GitHub](https://github.com/conda/grayskull) lists every recipe-generation flag
* [conda-forge](https://pydevtools.com/handbook/reference/conda-forge.md) explains the channel and its build infrastructure
* [uv vs. Pixi vs. Conda for scientific Python](https://pydevtools.com/handbook/explanation/uv-vs-pixi-vs-conda-for-scientific-python.md) compares the tools that install from conda-forge
