This release is a pre-release and may not be stable for production use.
unwrapt
Don't open it until you need it.
Every import in your program is a present your program unwraps
immediately, whether or not it uses what is inside. unwrapt leaves the
wrapping on until first use. It defers whole-module imports until the
moment they are touched, cutting start-up time, without changing your
application's code.
Python 3.15 adds lazy imports (PEP 810): a lazy import x statement,
and a module-level __lazy_modules__ list that keeps the imports it
names deferred while remaining ordinary valid code on older Pythons.
unwrapt brings the __lazy_modules__ behaviour to Python 3.12 to 3.14,
emulating it on top of wrapt
so that the same source behaves the same on every version, and it
ships a profile mode that measures what your imports actually cost and
reports what is worth deferring, on any version including 3.15.
It is a sibling project to wrapt and autowrapt, and works without changing your application code: with autowrapt installed, one environment variable activates it.
Status: beta, ahead of 1.0.0. Pre-releases are published to PyPI, and until 1.0.0 is final a plain
pip install unwraptpicks up the latest pre-release automatically, so there is no need to pin a specific version. Small API changes are still possible before 1.0.0. Reports of it working, or not, on real code, and of what confused or was missing, go to the issue tracker.
Installation
unwrapt is on PyPI:
$ pip install unwrapt
or with uv:
$ uv add unwrapt
autowrapt is the companion that injects unwrapt at interpreter startup without any application change. It is deliberately not a dependency of unwrapt; install it alongside the application you want to affect:
$ pip install autowrapt
At a glance
Your module declares what to defer, exactly as PEP 810 specifies, and the code stays valid on every Python version:
# app.py
__lazy_modules__ = ["xml.dom"]
import xml.dom
print("loaded at startup:", "xml.dom" in sys.modules) # False
xml.dom.Node # first use unwraps it
print("loaded after use:", "xml.dom" in sys.modules) # True
Run it with the bootstrap, and nothing else changes:
$ AUTOWRAPT_BOOTSTRAP=unwrapt python app.py
The import returns a lazy proxy instead of the module; the first use of the name imports the real module and swaps it into place, so after first use there is no residual overhead. On Python 3.15 the interpreter does all of this natively and unwrapt is not involved.
What it does
-
Shim mode (Python 3.12 to 3.14). With
AUTOWRAPT_BOOTSTRAP=unwrapt, unwrapt replacesbuiltins.__import__with a hook that returns a wrapt lazy proxy for each whole-module import your modules mark in their own__lazy_modules__, reifying on first use. Chained to whatever import wrappers were already installed, cheap on the pass-through path, and never raising from its own bookkeeping. -
Wrapping things you can't edit. A JSON configuration names imports to defer in modules whose source you do not control:
UNWRAPT_CONFIG=lazy.json. On Python 3.15 and later this is the only thing unwrapt does: the interpreter handles__lazy_modules__natively, unwrapt never touches it or native behaviour, and with no configuration it installs nothing at all. -
Peeking at the label.
UNWRAPT_DEBUG=1logs each deferred import to stderr, and the active mode at startup. -
Finding what is worth wrapping.
UNWRAPT_PROFILE=1records every import call of a real run, with timings and the tree of nested imports, and reports the candidates ranked by cumulative time, the heavy modules by self time, the import chains that explain why each one loaded, and the best cut in each chain. Every row shows both routes: what native 3.15 lazy imports can defer, and what the shim can, with the reasons spelled out. The report comes as text or JSON, and the choosing what to defer guide is the way to turn it into decisions.
What it is not
-
Not a replacement for PEP 810. On 3.15+ the interpreter is the mechanism; unwrapt only applies the external configuration there, and never reads
__lazy_modules__or alters native behaviour. -
Not a source rewriter. Nothing about your files changes; the profile mode only reports.
-
Not able to defer every import form.
from x import yandimport a.b as cstay eager under the shim on Pythons below 3.15: the attribute fetch resolves the proxy immediately. The profile flags exactly these rows, deferrable natively on 3.15 but not by the shim, and the guide covers the rewrite.
Known limitations
-
from x import yandimport a.b as cstay eager in shim mode. -
Only module-level imports are considered. Function bodies and class bodies are left alone (function-level imports are already deferred until the function runs).
-
A
try/except ImportErroraround an import is invisible to the hook: deferring a guarded optional dependency moves its ImportError to first use. Do not list those. -
Already-imported modules are returned as-is. In particular, wrapt itself and the standard library modules it imports (json, inspect, copy, subprocess and friends) are always in sys.modules before the hook goes in, so they can never be deferred in a process running unwrapt.
-
Older Pythons import eagerly without unwrapt. Never rely on laziness for correctness: import side effects and circular imports that only work lazily behave differently, and errors surface at first use instead of at the import statement.
-
Imports through
importlib.import_module, the C import API, and anything imported before the bootstrap runs are unaffected. -
There is per-import hook overhead for every import statement in the process while the shim is installed; the non-deferred path exits early and stays small, but it is not zero.
-
Proxies copied elsewhere before first use (assigned to another name, bound into a container) remain proxies and still work, with the small proxy overhead.
Documentation
Full documentation is at unwrapt.readthedocs.io. Start with the getting started page: everything on it can be pasted into a Python interpreter. For the profile mode, start with the choosing what to defer strategy guide, which turns the report into decisions step by step.
Developed with AI
This package was developed with the help of AI coding assistants, working to the author's design and direction, with the author reviewing what they produce. If you would rather not use software produced that way, that is understood, and this package is not for you.
Requirements
-
Python 3.12 or later.
-
wrapt 2.4.1 or later.
-
For zero-code injection: autowrapt 2.0 or later (not a dependency of unwrapt).
Issues
Bug reports and feature requests go to the issue tracker. A report is easiest to act on when it names the unwrapt and Python versions and includes a small application that reproduces the problem.
License
BSD 2-Clause, see LICENSE.
Metadata
Release files for unwrapt 1.0.0a1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| unwrapt-1.0.0a1.tar.gz | 46.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| unwrapt-1.0.0a1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 75.9 kB
Release files / unwrapt-1.0.0a1.tar.gz
| Download URL | unwrapt-1.0.0a1.tar.gz |
|---|---|
| Size | 46.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b4f598aaab827b1414898d8fe2cdad3980db03abc23bb22fb5f6fef0412a16cb
|
|
BLAKE2b-256 checksum How to use checksums |
a46623a8accdecad443f03933c741df822c2178905bfbbea89acf0718b228126
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logRelease files / unwrapt-1.0.0a1-py3-none-any.whl
| Download URL | unwrapt-1.0.0a1-py3-none-any.whl |
|---|---|
| Size | 29.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
28c2e842f4819f1004e8d4dd8c560623d5ae12ca8d0729e548cb4d72b643b0f2
|
|
BLAKE2b-256 checksum How to use checksums |
4467d61196dc56a2ea18662762b61cc916099ed3d73fe46db4b928908b474316
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logSecurity for unwrapt 1.0.0a1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for unwrapt-1.0.0a1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.