modupy
Start as one app. Grow into processes and services without re-architecting.
modupy helps you build a Python backend as a modular monolith: one codebase, split into modules that talk through events and public functions, never through each other's private code. On day one it is a plain FastAPI app. As your company grows, the same code runs one process per module, scales the busy parts, keeps events safe in your database, and splits a module off into its own service. Your events, listeners and module structure stay the same at every step. The outbox needs a few lines of session wiring, and splitting what modules share, such as tables, in-memory state and direct calls into another module, is your job.
pip install 'modupy[fastapi,cli]'
modupy installs the modulith package, so you import modulith, run the modulith command and configure [tool.modulith].
pip install modulith installs an unrelated project.
Quickstart · How it grows · Ready for AI · Examples · Docs
Pre-1.0 alpha: breaking changes can land in 0.x releases. See Status.
Why modupy
Most backends end up in one of two painful places:
- A big ball of mud. One app where everything imports everything. It is quick to start, then every change breaks something far away, and splitting it up means a rewrite.
- Microservices too early. Network calls, a dozen deploy pipelines and a platform team, long before your traffic needs them.
modupy is the path in between:
- Modules with walls. Each module is a Python package with a public API. Names that start with
_are private, andmodulith verifyfails your build when another module imports them. - Events between modules. A module publishes
OrderCreated, and every module that cares reacts to it. The publisher never needs to know who is listening. - One codebase, any shape. The same modules run in one process, in one process per module, or as separate services. Config picks the shape.
- No lost events. With the outbox on, an event published inside your database transaction is saved with your data, then retried until it is delivered or set aside as a dead letter you can replay.
How it grows with your company
| When your company… | You change… | You get… |
|---|---|---|
| ships its first version | nothing: uvicorn myapp.main:app |
one process, zero infrastructure |
| adds engineers and teams | modulith verify in CI |
boundaries nobody breaks by accident |
| can't afford to lose data | outbox = "postgres", your database URL and a session around each write |
events saved with your data, retried, and kept as dead letters if they keep failing |
| sees one feature get busy | --topology processes plus a worker count |
one process per module, and more of them for the busy one |
| outgrows one server | broker = "redis-streams" |
events that travel between machines |
| gives a team its own service | modulith extract <module> |
a standalone service built from one module |
Your events and listeners never change, and your URLs stay the same as long as main.py mounts each module's router at /<module>, as the quickstart does.
Ready for AI
Take LLM calls off the request path
Model calls are slow, rate-limited and sometimes fail. Put them in a listener in their own module, then turn on the outbox or give the module its own processes, so the call runs after the request has returned:
# myapp/assistant/__init__.py, a new module that nothing else imports
from modulith import listener
from myapp.contracts.events import OrderCreated
@listener
async def write_thank_you_note(event: OrderCreated) -> None:
note = await llm.generate(f"Write a two-line thank-you note for order {event.order_id}")
await notes.save(event.order_id, note) # llm and notes: your model client and your storage
The listener does not change in any of these setups:
- With the outbox on and the order published inside its database session (step 2), the note is written after the order commits, in the background, so placing an order stays fast. A failed model call is retried with backoff, up to 10 attempts by default, then kept as a dead letter.
modulith outbox dead-letter --retry-allreplays it by running the listener inside that command, so run it where your model credentials are. - In its own processes (step 3), a slow model cannot hold up your API, and
assistant = 4under[tool.modulith.workers]gives the assistant four of them. There the broker retries a failed call instead, 5 times by default, and keeps its dead letters itself. - As its own service (step 5),
modulith extract assistantgives it its own deploys, scaling and model API keys, or hands it to another team, while the rest of the app keeps publishing the same events.
In the default single process, with neither, publish() waits for every listener and re-raises the first error, so a slow or failing model call slows down or fails the order request.
With the outbox or a broker, events are delivered at least once, so make listeners safe to run twice, for example by skipping an order that already has a note.
Keep AI coding assistants inside the lines
Coding assistants write code fast, and they import whatever makes it work.
Say one fixes a bug in payments by adding from myapp.orders import _orders:
$ modulith verify
payments:
[ERROR] no-internal-imports: payments imports _orders from orders, reaching into orders's private package. Cross-module access must go through orders's public API. (myapp/payments/__init__.py:1)
1 error(s), 0 warning(s)
It exits with code 1, so CI fails.
Add it to your agent's instructions (AGENTS.md, CLAUDE.md) and the agent can catch the mistake before you see it.
Small modules with a public API also keep the context an assistant needs small, and modulith docs draws Mermaid diagrams of how your modules connect.
The 30-second pitch
Three modules and one shared contract.
orders publishes an event, payments and inventory react to it, and no module imports another.
An event is any class marked @event; a frozen dataclass is the recommended shape.
Every package also needs an __init__.py: the three modules below show theirs, and myapp/__init__.py and myapp/contracts/__init__.py are empty.
# myapp/contracts/events.py
from dataclasses import dataclass
from modulith import event
@event
@dataclass(frozen=True)
class OrderCreated:
order_id: str
@event
@dataclass(frozen=True)
class PaymentReceived:
order_id: str
# myapp/orders/__init__.py
from modulith import listener, publish
from myapp.contracts.events import OrderCreated, PaymentReceived
_orders: dict[str, str] = {} # order_id -> customer_id
_fulfilled: set[str] = set()
async def create_order(customer_id: str) -> str:
order_id = f"ord-{len(_orders) + 1}"
_orders[order_id] = customer_id # your real persistence goes here
await publish(OrderCreated(order_id=order_id))
return order_id
def is_fulfilled(order_id: str) -> bool:
return order_id in _fulfilled
@listener
async def on_payment(event: PaymentReceived) -> None:
"""Cross-module communication via events, not direct calls."""
_fulfilled.add(event.order_id) # your real fulfilment goes here
# Re-export the router onto the module package. Process-per-module mode serves
# HTTP by mounting each module package's `router` attribute under /<module>.
from myapp.orders.api import router as router # noqa: E402
# myapp/payments/__init__.py
from modulith import listener, publish
from myapp.contracts.events import OrderCreated, PaymentReceived
@listener
async def charge(event: OrderCreated) -> None:
await publish(PaymentReceived(order_id=event.order_id)) # your real charge goes here
The other three files: the orders routes, the inventory module and the app
# myapp/orders/api.py
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from myapp.orders import create_order, is_fulfilled
router = APIRouter()
class NewOrder(BaseModel):
customer_id: str
@router.post("")
async def post_order(body: NewOrder) -> dict[str, str]:
return {"order_id": await create_order(body.customer_id)}
@router.get("/{order_id}/fulfilment")
async def get_fulfilment(order_id: str) -> dict[str, str | bool]:
if not is_fulfilled(order_id):
raise HTTPException(status_code=404, detail="order not fulfilled")
return {"order_id": order_id, "fulfilled": True}
# myapp/inventory/__init__.py
from fastapi import APIRouter, HTTPException
from modulith import listener
from myapp.contracts.events import OrderCreated
_reserved: set[str] = set()
router = APIRouter()
@listener
async def reserve(event: OrderCreated) -> None:
_reserved.add(event.order_id) # your real stock reservation goes here
@router.get("/{order_id}")
async def get_reservation(order_id: str) -> dict[str, str | bool]:
if order_id not in _reserved:
raise HTTPException(status_code=404, detail="order not reserved")
return {"order_id": order_id, "reserved": True}
# myapp/main.py
import logging
from fastapi import FastAPI
from myapp.inventory import router as inventory_router
from myapp.orders import router as orders_router
logging.basicConfig(level=logging.INFO) # so the banner below is visible
app = FastAPI()
app.include_router(orders_router, prefix="/orders")
app.include_router(inventory_router, prefix="/inventory")
# That's it. Modules auto-discovered. Listeners auto-registered.
Run it:
$ uvicorn myapp.main:app
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:modulith:detected application package 'myapp'
INFO:modulith:discovered 4 module(s): contracts, inventory, orders, payments
INFO:modulith:outbox=memory, broker=memory, topology=single
INFO:modulith:outbox disabled — for durable delivery, set [tool.modulith].outbox = 'postgres' and outbox_url (or MODULITH_OUTBOX_URL), run `modulith migrate`, and wire the session and lifespan as modupy's README section "Never lose an event" shows
INFO:modulith:ready
modupy starts lazily, so its log lines appear on the first publish(): the order below.
$ curl -sX POST localhost:8000/orders \
-H 'content-type: application/json' -d '{"customer_id": "alice"}'
{"order_id":"ord-1"}
The order fanned out through events: payments charged it and published PaymentReceived, which marked it fulfilled, while inventory reserved the stock.
$ curl -s localhost:8000/orders/ord-1/fulfilment
{"order_id":"ord-1","fulfilled":true}
$ curl -s localhost:8000/inventory/ord-1
{"order_id":"ord-1","reserved":true}
Quickstart
Install
pip install 'modupy[fastapi,cli]'
fastapi brings FastAPI and uvicorn, and cli adds the modulith command.
The other extras are postgres (the durable outbox), redis and database (brokers for more than one machine), otel (tracing), test (what the pytest fixtures use) and all.
The core alone, pip install modupy, has one dependency: pluggy.
Lay out your project
myproject/
├── pyproject.toml # names your package: [project] name = "myapp"
└── myapp/
├── __init__.py
├── contracts/ # the events that modules share
│ ├── __init__.py
│ └── events.py
├── orders/ # a module: a package with a public API
│ ├── __init__.py # public functions, plus `router` re-exported from api.py
│ ├── _internal/ # private: other modules may not import it
│ ├── api.py # FastAPI routes
│ └── handlers.py # @listener functions, imported by __init__.py
├── inventory/
│ └── __init__.py
├── payments/
│ └── __init__.py
└── main.py # the FastAPI app
Three rules keep this working:
- Every module is a package with an
__init__.py, and its@listenerfunctions must be imported from there (for examplefrom . import handlers) so they register. - Names that start with
_are private to their module. - A module with routes re-exports its
routerfrom__init__.py, because a module running in its own process serves exactly thatrouterunder/<module>. A listener-only module likepaymentsneeds none.
The CLI finds your package through pyproject.toml, and one pytest setting lets your tests import it:
[project]
name = "myapp"
version = "0.1.0"
[tool.pytest.ini_options]
pythonpath = ["."]
Run it
uvicorn myapp.main:app --reload
Check the boundaries:
$ modulith verify
✓ no boundary violations
Run the same code, one process per module
$ modulith run myapp.main:app --topology=processes
modulith → process-per-module: 3 worker(s) [inventory:9001, orders:9002, payments:9003], reverse proxy on http://0.0.0.0:8000
Each module now runs in its own process behind one public port, so the URLs do not change:
$ curl -sX POST localhost:8000/orders \
-H 'content-type: application/json' -d '{"customer_id": "alice"}'
{"order_id":"ord-1"}
Events now cross process boundaries, so give the order a moment: until payments has charged it, the fulfilment route answers 404.
$ curl -s localhost:8000/orders/ord-1/fulfilment
{"order_id":"ord-1","fulfilled":true}
$ curl -s localhost:8000/inventory/ord-1
{"order_id":"ord-1","reserved":true}
contracts only defines events, so it gets no process of its own.
The warnings at startup are about the default broker's production settings, which step 3 covers.
Test it
modupy's pytest fixtures run a whole event flow in one process, with no server:
from modulith.testing import Scenario
def test_an_order_gets_paid(scenario: Scenario) -> None:
from myapp.contracts.events import PaymentReceived
from myapp.orders import create_order
payment = scenario.call(create_order, "alice").expect_event(PaymentReceived).within(seconds=2)
assert payment.order_id == "ord-1"
Save it under tests/; the imports sit inside the test because each test gets a fresh copy of your modules.
pip install pytest httpx2
pytest
Grow it, step by step
1. Keep modules independent
A module may call another module's public functions and use the events in contracts; modulith verify refuses the rest:
Put the boundary check in CI, so nobody, human or AI, quietly couples two modules:
# .github/workflows/ci.yml, in the job that installs your app's dependencies
- run: pip install 'modupy[cli]'
- run: modulith verify
As teams take ownership, give each module a manifest.
modupy checks it when it boots, and verify uses it to police dependencies and table ownership:
# myapp/orders/_manifest.py, optional
from modulith import declare_module
declare_module(
publishes=["OrderCreated"],
owns_tables=["orders_order"],
declared_dependencies=["contracts"],
)
Set strict_boundaries = true and modupy refuses to boot on any boundary violation, warnings included.
verify and --topology processes then fail before anything starts; in a single process, call bootstrap() at startup (step 2 shows where) to fail there rather than at the first publish().
Single-process modulith dev only warns, so a violation never stops your dev server.
2. Never lose an event
The outbox writes each event you publish inside a database session into the same transaction as your data.
If the transaction rolls back, the event is gone too; if the process crashes after the commit, the event is still there and is delivered after the app restarts.
A publish() outside a session is delivered directly and saves nothing, so the session wiring below is required.
With auto_discover = false the store is not bound at bootstrap: call outbox.configure() yourself, or a publish inside a session is also delivered directly, with one logged WARNING per process.
pip install 'modupy[postgres]' # for MySQL or SQLite, use 'modupy[database]'
[tool.modulith]
outbox = "postgres" # the SQL outbox: Postgres, MySQL or SQLite
outbox_url = "postgresql+asyncpg://app@db/app" # your business database, or MODULITH_OUTBOX_URL
modulith migrate # creates the outbox tables in that database
Publish inside your transaction:
# in myapp/orders/__init__.py
from uuid import uuid4
from modulith import publish
from modulith.builtin.outbox import bind_session, unbind_session
from myapp.contracts.events import OrderCreated
async def create_order(customer_id: str) -> str:
order_id = str(uuid4())
async with sessionmaker() as session: # your SQLAlchemy async sessionmaker
token = bind_session(session)
try:
session.add(Order(id=order_id, customer_id=customer_id)) # your model
await publish(OrderCreated(order_id=order_id))
await session.commit() # the order and its event are saved together
finally:
unbind_session(token)
return order_id
And start the outbox with your app:
# in myapp/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from modulith import bootstrap
from modulith.builtin import outbox
@asynccontextmanager
async def lifespan(app: FastAPI):
bootstrap()
outbox.start() # also retries what a crash left behind, once the dead process's lease ends
yield
await outbox.shutdown()
app = FastAPI(lifespan=lifespan)
Watch delivery, and fix it when a listener keeps failing:
modulith outbox status # incomplete, completed and dead-lettered counts
modulith outbox dead-letter --retry-all # replay the events whose listeners gave up
Retries are tuned under [tool.modulith.outbox_options].
examples/demo_app runs all of this on SQLite, then on Postgres.
3. Give busy modules their own processes
One flag runs every module in its own process, behind one public port:
modulith run myapp.main:app --topology processes
Give a busy module more processes:
[tool.modulith.workers]
reports = 4 # every other module keeps the default of 1
Each process has its own memory and the proxy spreads requests across them, so keep that module's state in your database.
A supervisor restarts a crashed worker after 1, 2, 4, 8, then 16 seconds, and gives up after six crashes in a row, leaving that module down until you fix it.
Events between processes travel through a built-in queue on the same machine, so there is still nothing extra to install; in production, set its state_dir under [tool.modulith.broker_options] (DEPLOYMENT.md).
4. Spread across machines
Swap the broker, and events travel between hosts:
[tool.modulith]
topology = "processes"
broker = "redis-streams" # reads REDIS_URL; pip install 'modupy[redis]'
Rather not run Redis?
broker = "database" puts the queue in Postgres or MySQL instead: install modupy[database] and set url under [tool.modulith.broker_options] (Cookbook).
Delivery is at least once, so listeners must be safe to run twice.
Then place modules on different machines: modulith k8s-manifest (step 6) writes one Deployment per module, or start a single module anywhere with MODULITH_MODULE=reports MODULITH_APP_PACKAGE=myapp uvicorn modulith._worker:create_app --factory.
5. Split off a service
When a module needs its own deploys, its own team or its own hardware, extract it:
modulith extract payments --output payments-service
cd payments-service
cp .env.example .env # then fill in your broker and database settings
docker build -t payments-service .
docker run --env-file .env -p 8000:8000 payments-service
This builds on step 4: the rest of the app must run with topology = "processes" and a shared broker, which extract also reads to pick the service's drivers.
The service gets the module, its contracts, a pyproject.toml, a Dockerfile, a README.md and a .env.example.
The rest of the app keeps publishing the same events through the broker, and the new service consumes them. Then delete the module from the main app, because while both run they split its events.
extract refuses a module that still reaches into other modules or shares their tables, so the check from step 1 is what makes splitting safe.
6. Run it in production
modulith k8s-manifest --image myapp:1.0.0 # a Deployment and a Service per module, plus one Ingress
modulith openapi --output openapi.json # one OpenAPI spec covering every module
modulith doctor # architecture and operations health checks
k8s-manifest needs the shared broker from step 4.
Install modupy[otel] and configure an OpenTelemetry tracer provider, and every publish and every listener call gets a span.
Already have a codebase?
Adopt modupy one module at a time:
modulith audit # proposes modules and writes MIGRATION.md with a readiness score (--force replaces an existing one)
modulith verify --update-baseline # accepts today's violations; commit .modulith-baseline.json
modulith verify --mode=ratchet # from now on, fails only on new violations
Keep strict_boundaries off while you adopt: with it on, verify fails at boot on the very violations the baseline should record.
MIGRATION_GUIDE.md walks through the whole move.
Configuration
Everything lives in pyproject.toml, and the defaults need no changes to get started:
[tool.modulith]
package = "myapp" # default: [project].name
outbox = "postgres" # default: "memory"
outbox_url = "postgresql+asyncpg://app@db/app"
topology = "processes" # default: "single"
broker = "redis-streams" # with processes, default "shm" ("database" if a broker url is set)
strict_boundaries = true # refuse to boot on any boundary violation
[tool.modulith.workers]
default = 1
reports = 4
Every setting directly under [tool.modulith] can also come from an environment variable, such as MODULITH_OUTBOX_URL or MODULITH_BROKER.
API_REFERENCE.md lists every setting and its default, ARCHITECTURE.md explains how settings resolve (§3), outbox retries (§7) and brokers (§8), and DEPLOYMENT.md covers securing the actuator endpoints.
CLI
| Command | What it does | When you use it |
|---|---|---|
modulith dev myapp.main:app |
runs with auto-reload and prints boundary warnings at startup | local development |
modulith run myapp.main:app |
runs for production; add --topology processes for one process per module |
deploying |
modulith verify |
checks module boundaries and exits 1 on a violation | CI, and AI agents |
modulith info |
shows the detected package, modules and settings | finding your way |
modulith docs |
writes Mermaid diagrams of your modules to docs/modulith |
onboarding |
modulith doctor |
reports architecture and operations health | reviews and incidents |
modulith migrate |
creates the outbox and database-broker tables | deploying |
modulith outbox status |
counts incomplete, completed and dead-lettered events | operations |
modulith outbox failing |
lists events that are failing but not yet dead-lettered, with their last error and next retry time | incidents |
modulith outbox dead-letter |
lists dead-lettered events, or replays them with --retry-all |
incidents |
modulith broker dead-letter |
lists dead-lettered broker deliveries, or resubmits them with --retry-all (database, shm and redis-streams brokers; redis-streams resubmits only targets with one consumer group) |
incidents |
modulith broker drop-group <group> |
removes a retired module's consumer group | operations |
modulith extract <module> |
turns one module into a standalone service | splitting a service off |
modulith k8s-manifest |
writes Kubernetes manifests, one Deployment per module | deploying |
modulith openapi |
merges every module's API into one OpenAPI file | API portals and clients |
modulith audit |
assesses an existing codebase and writes MIGRATION.md; an existing file is replaced only with --force |
adopting modupy |
Commands that inspect your modules find your package through [tool.modulith].package, else [project].name.
disabled_rules = ["use-contracts"] under [tool.modulith.verify] turns off boundary rules by their rule name, whether a built-in rule or one a plugin contributes, in verify, doctor and the strict_boundaries startup check; parse-error cannot be turned off.
Every command except audit and migrate imports your modules, verify included, so run them only on code you trust and where your app's dependencies are installed.
modulith dev and modulith run take --log-level (default info) for the supervisor and every worker; in one process they hand over to uvicorn, so modupy's startup banner shows only if your app configures logging, as myapp/main.py does.
With strict_boundaries = true, verify, run --topology processes and dev --topology=processes fail on warnings too; a single-process run fails when the app first boots modupy, and single-process modulith dev stays warn-only by design.
Exit codes: 0 for success, 1 for violations or bad input, 2 for an internal error or a CLI usage error.
Examples
CI runs every command in every example's README exactly as written, from a fresh install of the built package.
| Example | Size | What it shows |
|---|---|---|
quickstart |
small: 3 modules | the code above, in one process and in one process per module |
demo_app |
mid: a shop with a database | the durable outbox on SQLite, modulith migrate, a scaled module, then Postgres and Redis |
marketplace |
large: 7 modules | a payment saga that undoes itself on failure, dead-letter recovery, a team boundary rule shipped as a plugin, tracing, Kubernetes and extracting a service |
Single-file plugin examples: a verifier rule, a Redis Streams broker and a storage serializer.
Is modupy right for you?
It fits teams of roughly 3 to 15 engineers building a Python product, often B2B SaaS, who want to put off microservices for as long as possible without painting themselves into a corner. If that is not you, it may not be the right fit; SPEC.md Part II explains who it is for.
| modupy | FastAPI + folders | FastAPI + Celery + import-linter | Microservices | |
|---|---|---|---|---|
| Module boundaries | ✓ enforced | ✗ convention only | ✓ enforced by import-linter | ✓ enforced by the network |
| Events between modules | ✓ in-process or through a broker | ✗ do it yourself | ✓ Celery | ✓ broker only |
| Transactional outbox | ✓ built in | ✗ do it yourself | ✗ do it yourself | ✗ do it yourself, per service |
| One process per module | ✓ one flag | ✗ | ✗ Celery workers split by task queue, not by module | n/a, already separate |
| Split a module into a service | ✓ modulith extract |
✗ by hand | ✗ by hand | n/a, already separate |
| Adopting on an existing codebase | ✓ baseline and ratchet | n/a | ✓ | ✗ a rewrite |
| Operational complexity | low in one process; process mode adds a supervisor and a reverse proxy | lowest | medium | highest |
tach and import-linter enforce import boundaries as a lint step or in CI (tach check, lint-imports), with no events, outbox, runtime or extraction.
Pick one of them when import rules are all you need.
modulith verify covers boundaries too, and modupy adds the runtime pieces: events, the outbox, one process per module and modulith extract.
modupy is inspired by Spring Modulith.
Status
modupy is a pre-1.0 alpha: breaking changes may land in 0.x minor releases, and each one is listed in CHANGELOG.md.
The core, the transactional outbox, the tooling and the process-per-module runtime are code-complete and pass pytest, mypy --strict and ruff; more adapters follow after 1.0, as users ask for them (ROADMAP.md).
Every pull request and every push to main runs more than 2,600 hermetic tests on Python 3.11, 3.12, 3.13 and 3.14 (Linux, with the SHM broker also on macOS and Windows), plus more than 140 integration tests against real Postgres, MySQL and Redis containers, which include every example README run from the built wheel.
STABILITY.md states what stays stable across 0.x releases.
Documentation
- SPEC.md: the design document, with the reasoning behind modupy's design; where it and the code disagree, the code and docs/STABILITY.md are right
- docs/ARCHITECTURE.md: how modupy works inside, from the runtime and plugins to the outbox, cross-process delivery and the verifier
- docs/COOKBOOK.md: step-by-step recipes for common jobs
- docs/DEPLOYMENT.md: Docker and Kubernetes, scaling, health probes and operations
- docs/API_REFERENCE.md: the public API, generated from docstrings
- docs/STABILITY.md: what is guaranteed across 0.x releases
- MIGRATION_GUIDE.md: adopting modupy in an existing codebase
- ROADMAP.md: what comes next
- CHANGELOG.md: what changed in each release
Contributing
The design is opinionated, so please read SPEC.md before opening a large PR. The plugin contract (13 hookspecs, 5 protocols) is the most stable part of the project: additions are easy, and signature changes need strong justification. Development setup and both test suites, the hermetic default and the Docker-backed integration suite, are in CONTRIBUTING.md.
License
Copyright 2026 Jean Sossmeier. Apache-2.0, see LICENSE.
Metadata
Release files for modupy 0.12.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| modupy-0.12.0.tar.gz | 1.4 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| modupy-0.12.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.8 MB
Release files / modupy-0.12.0.tar.gz
| Download URL | modupy-0.12.0.tar.gz |
|---|---|
| Size | 1.4 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bd6edf76e10ef684e82f1634814d90253f36694202a003d7c7aa14bc2f141ef3
|
|
BLAKE2b-256 checksum How to use checksums |
939a335a142dbb1a0d6992160be9bf9cd4be784c2699183d908b9c9a7e9afb03
|
| 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 6, 2026.
Transparency logRelease files / modupy-0.12.0-py3-none-any.whl
| Download URL | modupy-0.12.0-py3-none-any.whl |
|---|---|
| Size | 450.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d3f5f447d84c43d6b6bca34967f56a99fce3c6982714cac468392a574c9b7a22
|
|
BLAKE2b-256 checksum How to use checksums |
9a95218ea88fdaa8ac88f36f8fa718f15975a3d7ec131fcbb31db24cd7395566
|
| 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 6, 2026.
Transparency log