Runtime capability registry for optional Python dependencies
Project description
mypantry
mypantry is a lightweight (~400 lines of code) runtime capability registry for optional Python dependencies. It discovers dependency groups from your pyproject.toml, probes which packages are actually installed, and gives you a clean API to check availability, import modules safely, and guard functions with decorators.
The entire source code is intentionally small and readable. You are encouraged to read it: _registry.py + _probe.py are all you need to understand how it works.
Why mypantry?
When your library supports optional features powered by third-party packages, you need to:
| Problem | Without mypantry | With mypantry |
|---|---|---|
| Check if a package is installed | try: import pkg scattered everywhere |
pantry.has("pkg") |
| Import with fallback | Repeated try/except blocks | pantry.get("pkg") |
| Fail with clear message | Custom error handling per feature | pantry["pkg"] raises with install instructions |
| Guard a function | Manual checks at every entry point | @pantry("pkg1", "pkg2") |
| Show what's available | Roll your own reporting | pantry.report() |
| Break circular imports | Move imports inside functions | pantry.lazy_import("my.module.Class") |
Zero configuration — just declare optional dependencies in pyproject.toml as you normally would.
Installation
pip install mypantry
Only runtime dependency: packaging (454 KB, zero transitive deps, already present in most Python environments).
Setup
Declare optional dependencies in your pyproject.toml:
[project]
name = "my-awesome-lib"
[project.optional-dependencies]
data = ["pandas>=2.0", "numpy>=1.24"]
imaging = ["pillow>=10.0", "wand"]
ml = ["torch", "scikit-learn"]
cache = ["redis>=5.0"]
That's it. No configuration files, no plugin registration, no setup code.
Quick Start
import pantry
# Strict import — raises RuntimeError with install instructions if missing
PIL = pantry["pillow"]
img = PIL.Image.open("photo.jpg")
# Safe import — returns None (or a custom default) if missing
np = pantry.get("numpy")
redis = pantry.get("redis", None)
# Check availability (single or multiple)
if pantry.has("pillow"):
...
if pantry.has("numpy", "pandas"): # all must be available
...
# Guard a function — fails at call-time, not import-time
@pantry("numpy", "pandas")
def analyze(data):
import numpy as np
import pandas as pd
...
# See what's available
print(pantry.report())
Output of report():
pantry report
──────────────────────────────────────────────────────
group package module version ok
data pandas pandas 2.1.4 ✓
data numpy numpy 1.26.4 ✓
imaging pillow PIL 10.4.0 ✓
imaging wand wand - ✗
ml torch torch - ✗
ml scikit-learn sklearn - ✗
cache redis redis - ✗
──────────────────────────────────────────────────────
available: 3/7
How It Works
When you import pantry, the module:
- Walks up from cwd to find
pyproject.toml - Reads
[project.optional-dependencies] - Probes each package (installed? importable? version?)
- Replaces itself with a
Pantryinstance — so you use it directly
Smart module name resolution handles the pip-name-to-import-name mapping automatically (pillow -> PIL, scikit-learn -> sklearn, python-dateutil -> dateutil, etc.).
Explicit Construction
When you need to target a specific pyproject.toml:
from pantry import Pantry
p = Pantry.from_pyproject("path/to/pyproject.toml")
p = Pantry.discover(start="/my/project")
Lazy Import — Breaking Circular Dependencies
Pantry has a second, independent feature for your own project modules: deferred imports that break circular dependency chains.
This feature does not make external dependencies (numpy, torch, etc.) lazy. It is specifically designed to break circular imports between your own modules.
The Problem
# myapp/module_a.py
from myapp.module_b import Helper # module_b imports module_a -> circular!
class Service:
def run(self):
return Helper()
# myapp/module_b.py
from myapp.module_a import Service # module_a imports module_b -> circular!
class Helper:
def check(self):
return Service()
The Solution
# myapp/module_a.py
import pantry
pantry.lazy_import("myapp.module_b.Helper")
class Service:
def run(self):
Helper = pantry["myapp.module_b.Helper"] # import happens here
return Helper()
lazy_import just registers the name — no import occurs. The actual import
happens on first pantry["..."] access, when all modules are fully loaded.
Results are cached after first resolution.
This is a bridge toward PEP 690 (Lazy Imports).
When PEP 690 becomes available, migration is straightforward: remove lazy_import()
calls and replace pantry["path"] with standard imports.
Note: Lazy imports are completely separate from external dependencies.
has(),get(),report(), and the decorator only know about pyproject.toml packages.
When to Use mypantry
Good fit:
- Libraries with many optional features (data science, ML, web, scientific, etc.)
- Projects with interconnected modules that suffer from circular imports
- Applications that need clear "install X for feature Y" error messages
- Frameworks where different users have different optional packages installed
Not needed:
- Simple scripts or single-file projects
- Projects with no optional dependencies
- Projects where all dependencies are always required
API Summary
External Dependencies (pyproject.toml)
| Syntax | Description |
|---|---|
pantry["pkg"] |
Import module; raise RuntimeError if missing |
pantry.get("pkg") |
Import module; return None if missing |
pantry.get("pkg", default) |
Import module; return default if missing |
pantry.has("pkg") |
True if installed and importable |
pantry.has("p1", "p2") |
True if all are available |
pantry.has_group("grp") |
True if any in group is available |
@pantry("p1", "p2") |
Decorator; RuntimeError at call-time if missing |
pantry.report() |
Formatted availability table |
Pantry.discover() |
Explicit construction from auto-discovered pyproject.toml |
Pantry.from_pyproject(path) |
Explicit construction from a specific file |
Lazy Import (own modules)
| Syntax | Description |
|---|---|
pantry.lazy_import("a.b.C") |
Register for deferred import |
pantry["a.b.C"] |
Resolve on first access (cached) |
Testing
| Syntax | Description |
|---|---|
with pantry.simulate_missing("pkg") |
Temporarily hide packages for testing |
Key Features
- Lightweight — ~400 lines of code, single dependency (
packaging) - Zero config — reads standard
pyproject.toml, no extra files or setup - Lazy probing —
import pantryis fast: modules are imported only on first access - Module-as-instance —
import pantrygives you a ready-to-use object - Smart resolution — pip names mapped to import names automatically
- Multiple access patterns — strict (
[]), safe (.get()), check (.has()) - Decorator guards — fail at call-time with clear install instructions
- Group awareness — check entire dependency groups at once
- Lazy import — break circular dependencies in your own modules (PEP 690 bridge)
- simulate_missing — context manager for testing fallback behavior
- Availability report — formatted table for diagnostics
- Fully typed — PEP 561
py.typedmarker included
Troubleshooting
No pyproject.toml found?
Pantry walks up from the current working directory. If no pyproject.toml is found
(e.g. in a REPL), an empty Pantry is created — import pantry never fails.
has() returns False for everything, report() shows "(no optional dependencies declared)".
Non-standard import names?
Pantry resolves pip names to import names automatically (pillow -> PIL, etc.)
using distribution metadata. This works for the vast majority of PyPI packages.
For the rare edge case, use the module's actual import name directly.
Transitive dependencies missing?
Pantry only probes packages explicitly listed in [project.optional-dependencies].
If package A requires package B, and B is missing, Pantry reports A as unavailable
(the import fails). List the packages your code directly imports.
Documentation
Full documentation: mypantry.readthedocs.io
Testing
pip install mypantry[dev]
pytest
Repository Structure
genro-pantry/
├── src/pantry/
│ ├── __init__.py # Module-as-instance bootstrap (33 lines)
│ ├── _discovery.py # pyproject.toml discovery and parsing (34 lines)
│ ├── _probe.py # Package probing and module name resolution (97 lines)
│ ├── _registry.py # Pantry class — the entire API (247 lines)
│ └── py.typed # PEP 561 marker
├── tests/ # 553 lines, 71 tests, 95% coverage
├── docs/
├── pyproject.toml
└── LICENSE
Project Status
- Status: Beta
- Python: 3.11, 3.12, 3.13
- License: MIT
Contributing
Contributions and feedback are welcome, especially on edge cases with lazy imports. Please open an issue first to discuss what you'd like to change.
License
Copyright (c) 2025 Softwell S.r.l. — MIT License.
Project details
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 mypantry-0.4.0.tar.gz.
File metadata
- Download URL: mypantry-0.4.0.tar.gz
- Upload date:
- Size: 28.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b0441e26e6d632a9c1fc72b41f97f30d34c5adee109cfdf19a73639de024cd9
|
|
| MD5 |
02aba1eba579ff78004f46653a514048
|
|
| BLAKE2b-256 |
16466ce7be65401813c88c75574dddfaf309ddccfcc5b278b94308d785be1fba
|
Provenance
The following attestation bundles were made for mypantry-0.4.0.tar.gz:
Publisher:
publish.yml on genropy/genro-pantry
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mypantry-0.4.0.tar.gz -
Subject digest:
3b0441e26e6d632a9c1fc72b41f97f30d34c5adee109cfdf19a73639de024cd9 - Sigstore transparency entry: 1191434441
- Sigstore integration time:
-
Permalink:
genropy/genro-pantry@fba097bf8789eee8fddbe4741a36af04d53ebf0a -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/genropy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fba097bf8789eee8fddbe4741a36af04d53ebf0a -
Trigger Event:
push
-
Statement type:
File details
Details for the file mypantry-0.4.0-py3-none-any.whl.
File metadata
- Download URL: mypantry-0.4.0-py3-none-any.whl
- Upload date:
- Size: 12.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
905573561c0c13c3b9142c40a5d9b3e0b79578558521fb94d96b452d2d4204c0
|
|
| MD5 |
08550924462996a3453c691e85d8f9c2
|
|
| BLAKE2b-256 |
3aadbfd2a457a1a8e304c173b986f69e64aaaf8a2e82e07c2f3ac25947e96d06
|
Provenance
The following attestation bundles were made for mypantry-0.4.0-py3-none-any.whl:
Publisher:
publish.yml on genropy/genro-pantry
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mypantry-0.4.0-py3-none-any.whl -
Subject digest:
905573561c0c13c3b9142c40a5d9b3e0b79578558521fb94d96b452d2d4204c0 - Sigstore transparency entry: 1191434444
- Sigstore integration time:
-
Permalink:
genropy/genro-pantry@fba097bf8789eee8fddbe4741a36af04d53ebf0a -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/genropy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fba097bf8789eee8fddbe4741a36af04d53ebf0a -
Trigger Event:
push
-
Statement type: