Skip to main content

geninit

Keeping __init__.py files up to date gets tedious once a package has more than a few modules. geninit takes care of generating those files while still letting you decide what belongs in your public API.

The geninit command itself requires Python 3.15 or newer, but can generate eager __init__.py files for projects that support older Python versions.

Installation

The simplest way to install geninit is as an isolated uv tool:

uv tool install --python 3.15 geninit

You can then run geninit from any project. If you only want to try it once, use uvx instead:

uvx --python 3.15 geninit --check

For a project that keeps its development tools in uv.lock, add geninit to the development dependency group:

uv add --dev geninit

Commands in the rest of this README can then be written as uv run --locked --no-sync geninit ....

Getting started

Add a [tool.geninit] table to your pyproject.toml and list the packages you want geninit to manage:

[project]
requires-python = ">=3.14"

[tool.geninit]
roots = ["src/acme"]

Run geninit from the directory containing pyproject.toml:

geninit

The first run creates the necessary __init__.py files. Later runs update the managed sections when modules or exports change.

You can also pass additional package roots on the command line:

geninit src/acme_plugins

Command-line roots are added to the roots from pyproject.toml.

Choosing what to export

By default, a normal child module is protected: the package exposes the module itself, but does not copy names from that module into the package namespace. Modules whose names start with an underscore are private by default.

Use these declarations in the handwritten part of __init__.py when you want different behavior:

__public__ = ("api",)
__protected__ = ("models",)
__private__ = ("legacy",)

# <geninit>
# </geninit>
  • A public child exposes the module and the names listed in that child's __all__.
  • A protected child exposes only the module.
  • A private child is not exposed by the package.

For example, src/acme/api.py might contain:

__all__ = ("Client", "connect")


class Client: ...


def connect() -> Client:
    return Client()

With api declared public, users can then write either from acme import Client or from acme import api.

The __public__, __protected__, __private__, and child __all__ declarations must contain literal strings. geninit reports an error for unknown children, duplicate names, overlapping declarations, or conflicting exports.

Managed files

geninit only replaces content between these markers:

# <geninit>
# </geninit>

New and empty __init__.py files are generated automatically. geninit will not overwrite a nonempty file unless it already contains exactly one matching pair of markers. To adapt an existing __init__.py file, add the empty marker pair where you want the generated section to appear, then run geninit.

Excluding files and forcing eager imports

exclude removes matching paths from discovery. eager forces selected modules to use ordinary eager imports:

[tool.geninit]
roots = ["src/acme"]
exclude = ["**/tests/**", "**/fixtures/**"]
eager = ["core.py", "plugins/builtin.py"]

Both options use paths relative to their package root.

geninit reads [project].requires-python to decide which import syntax is safe. If every supported version is Python 3.15 or newer, imports are lazy unless they match eager. If the project supports an earlier version, or the version constraint is missing, all generated imports are eager.

Checking without changing files

Use --check in CI or whenever you only want to verify that the generated files are current:

geninit --check

Use --diff to print the changes geninit would make:

geninit --diff

The command uses these exit codes:

  • 0 means every managed file was already current.
  • 1 means configuration, input, or generation failed.
  • 2 means files were changed, or --check/--diff found stale files.

The normal write command deliberately exits with status 2 after changing files. This makes generated changes visible in pre-commit hooks instead of letting a commit continue with unstaged output.

Using geninit with prek or pre-commit

For a uv-managed project, add this local hook to your pre-commit config:

[[repos]]
repo = "local"

[[repos.hooks]]
id = "geninit"
name = "Generate package initializers"
entry = "uv run --locked --no-sync geninit"
language = "system"
pass_filenames = false
always_run = true
stages = ["pre-commit"]

pass_filenames = false is important: geninit works from package roots rather than a list of staged files. always_run = true makes sure module deletions and configuration-only changes are noticed too.

If the hook updates anything, review the changes, stage them, and commit again.

If you are using ruff to format and lint your code, be sure to put geninit before ruff in pre-commit. Then, make sure ruff is available in the hook environment so that geninit can use the target project's Ruff configuration to format and sort the generated section.

CI example

A typical uv-based CI job can verify generated files before running the rest of the checks:

uv sync --locked
uv run --locked --no-sync geninit --check
uv run --locked --no-sync ruff format --check .
uv run --locked --no-sync ruff check .

If an initializer is stale, geninit --check exits with status 2 and the job fails without modifying the checkout.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

geninit-0.2.0.tar.gz (13.6 kB view details)

Uploaded Source

Built Distribution

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

geninit-0.2.0-py3-none-any.whl (16.9 kB view details)

Uploaded Python 3

File details

Details for the file geninit-0.2.0.tar.gz.

File metadata

  • Download URL: geninit-0.2.0.tar.gz
  • Upload date:
  • Size: 13.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for geninit-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e20f09fe0a1d4bb5c19bf78c894d0e1209752ffb72eec47a91a99fba845127ee
MD5 dc60c9487235751b362bb79fab3d4787
BLAKE2b-256 4c1ab155be324bbbc7df2470194d927713ae3a1b9f9d731806655d31825338e0

See more details on using hashes here.

File details

Details for the file geninit-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: geninit-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 16.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for geninit-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1a2afe1360ee28c3d929030cca5297e442d91f83f953625c5b7dd185ae645aa1
MD5 b2fcf1ab5310e90a47ff0f8a68486c50
BLAKE2b-256 03ccc5e3e1e77e843b7b83ff20655ca9cb39c9ed2afe60f89e5900d6787f5ce0

See more details on using hashes here.

Release history Release notifications | RSS feed

0.3.0

2 files

This release

0.2.0 This release

2 files

0.1.3

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