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",)
__flattened__ = ("implementation",)
__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 flattened child exposes only the names listed in the child's
__all__. - 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.
Declaring api flattened instead would support the first form without adding
api to the package's generated __all__ or propagating that child name
through public parent packages.
This controls the generated API; importing a child's contents may still cause
Python to bind the child as a runtime package attribute.
The __public__, __protected__, __flattened__, __private__, and child
__all__ declarations must contain literal strings, must be unique and must
resolve to known symbols.
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:
0means every managed file was already current.1means configuration, input, or generation failed.2means files were changed, or--check/--difffound 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file geninit-0.3.0.tar.gz.
File metadata
- Download URL: geninit-0.3.0.tar.gz
- Upload date:
- Size: 13.9 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75ca6e650c5c3659c153e9d931b7e5842fa897560b39c94456eeb7a9c84e5daa
|
|
| MD5 |
5e43415d5de369dd0ad4cad289f6bf5e
|
|
| BLAKE2b-256 |
5097bb633232a9072e357879015914dd24ad85c305dec30117fc362f6e1b78a7
|
File details
Details for the file geninit-0.3.0-py3-none-any.whl.
File metadata
- Download URL: geninit-0.3.0-py3-none-any.whl
- Upload date:
- Size: 17.2 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c24cc422db618ab141e68b220aa4a08a0838447d196afe69e6626109da9ff44f
|
|
| MD5 |
98a4145de1aa6bd7606927d59b80b57f
|
|
| BLAKE2b-256 |
127cf93bb629d100c232a5bcd1646c953b9119f571cce80adfc0649b4e0a1210
|