Exportify manages the public API of your Python packages: it analyzes your source tree, applies a YAML rule set to decide which symbols to export, and writes or updates __init__.py files with lazy-loading imports, __all__ declarations, and TYPE_CHECKING blocks — all in a single command.
pip install exportifyPython 3.12 or later is required.
Run exportify init in your project root to create a starter config file:
exportify initThis writes .exportify/config.yaml with a set of default rules that work for most packages:
schema_version: "1.0"
rules:
- name: exclude-private-members
priority: 900
description: Exclude private members (starting with underscore)
match:
name_pattern: ^_.*
action: exclude
- name: propagate-exceptions
priority: 800
description: Propagate exception classes to root package
match:
name_pattern: .*Error$|.*Exception$|.*Warning$
member_type: class
action: include
propagate: root
- name: include-constants
priority: 700
description: Include SCREAMING_SNAKE_CASE constants
match:
name_pattern: ^[A-Z][A-Z0-9_]+$
member_type: constant
action: include
propagate: parent
- name: include-public-functions
priority: 500
description: Include public functions
match:
member_type: function
action: include
propagate: parent
- name: include-public-classes
priority: 500
description: Include public classes
match:
member_type: class
action: include
propagate: parentTo preview what would be generated without writing anything:
exportify init --dry-runTo overwrite an existing config:
exportify init --forceFor full documentation on the config format and available rule fields, see the Rule Engine reference.
Before making any changes, see what exportify finds:
exportify check --verboseThis runs four checks:
- lateimports — verifies that any existing
lateimport()/LateImportcalls resolve to real modules - dynamic-imports — verifies
_dynamic_importsentries in__init__.pyfiles are consistent - module-all — checks that
__all__in regular modules matches your export rules - package-all — checks that
__all__and exports in__init__.pyfiles are consistent
The lateimports check is automatically skipped if lateimport is not in your project dependencies.
Example output:
Checking src/mypackage...
[OK] lateimports: 0 issues
[WARN] module-all: 3 modules missing __all__
[FAIL] package-all: src/mypackage/core/__init__.py: __all__ contains 'internal_helper' (not in rules)
To fail CI on warnings as well as errors, use --strict:
exportify check --strictTo get machine-readable output:
exportify check --jsonAlign your __init__.py files and __all__ declarations with your rules:
# Preview what would be changed
exportify sync --dry-run
# Apply the changes (creates and updates files)
exportify syncThe sync command:
- Creates missing
__init__.pyfiles in package directories - Updates
_dynamic_importsand__all__in__init__.pyfiles - Updates
__all__in regular modules to match export rules - Preserves manually written code above the managed exports sentinel
Generated files contain:
__all__— the list of exported names, determined by your rules_dynamic_imports— a mapping used bylateimportfor lazy loading__getattr__— the lazy-loading hookTYPE_CHECKINGblock — type-only imports for type checker compatibility
To sync a specific module or package only:
exportify sync src/mypackage/coreTo sync only __all__ in regular modules:
exportify sync --module-allConfirm the current state of your project:
exportify doctor --shortOutput:
[bold]Exportify Status Snapshot[/bold]
Cache Status:
Entries: 42/42 valid
Hit rate: 98.0%
Configuration:
Rules: ✓ .exportify/config.yaml
System:
Status: Ready
For a deeper health check including cache health, rule configuration, export conflicts, and performance:
exportify doctorHere is a typical generated __init__.py for a package called mypackage.utils:
# SPDX-FileCopyrightText: 2026 Acme Corp.
# SPDX-License-Identifier: MIT
"""Utility functions for mypackage."""
from __future__ import annotations
from typing import TYPE_CHECKING
# TYPE_CHECKING block — only imported by type checkers, not at runtime
if TYPE_CHECKING:
from mypackage.utils.formatting import format_output
from mypackage.utils.parsing import parse_config
# === MANAGED EXPORTS ===
# This section is automatically managed by exportify.
# Manual edits below this line will be overwritten on the next `sync` run.
from types import MappingProxyType
from lateimport import create_late_getattr
# Maps export name -> (package, module)
_dynamic_imports: MappingProxyType[str, tuple[str, str]] = MappingProxyType({
"format_output": (__spec__.parent, "formatting"),
"parse_config": (__spec__.parent, "parsing"),
})
# Installs __getattr__ for lazy loading: attributes are only imported when first accessed
__getattr__ = create_late_getattr(_dynamic_imports, globals(), __name__)
# Public API surface
__all__ = (
"format_output",
"parse_config",
)Key points:
- Code above
# === MANAGED EXPORTS ===is yours — it is preserved across re-runs. - The
_dynamic_importsdict maps each export name to its source module. Imports happen on first attribute access (lazy loading), so importing the package is fast even with many exports. TYPE_CHECKINGimports make type checkers aware of the exported names without incurring runtime import cost.__all__is the authoritative public API list; it tells tools likefrom mypackage.utils import *what to expose.
The sentinel line # === MANAGED EXPORTS === divides each __init__.py into two zones:
Preserved zone (above the sentinel) — edited freely, never touched by exportify:
"""My package docstring."""
from __future__ import annotations
__version__ = "1.2.3"
# Custom initialization code here
_registry: dict[str, type] = {}
# === MANAGED EXPORTS ===Managed zone (below the sentinel) — fully controlled by exportify, rewritten on each sync run.
If a file has no sentinel, exportify treats the entire file as preserved and will not write to it unless you add the sentinel manually or run sync (which creates the sentinel for new or empty files).
- CLI Reference — complete command and flag reference
- Rule Engine — full rule schema, priority bands, match criteria, and examples
- Caching — how the analysis cache works and when to clear it
- Init / Configuration — details on the
initcommand and the Python API for generating configs