This release is a pre-release and may not be stable for production use.
Deadtrace
Deadtrace is an open-source, explainable dead-code analyzer for Python applications. It models framework registrations, dependency injection, callbacks, lifecycle hooks, and test consumers instead of treating “no textual references” as proof that code can be deleted.
Public alpha: scans are static and read-only. Findings are review units, never claims that deletion is safe. APIs and artifact schemas may change during the alpha series.
What works now
- world-local reachability with separate resolved and conservative execution;
- Python references, closures, inheritance and
super(), method overrides, decorators, and evaluated annotations, with conservative protection for values of unknown type and for functions registered by decorators of frameworks that are not modeled; - special methods, hooks that external base classes may call on their subclasses, modules
and symbols named by strings, and modules imported with
importlib.import_module; - module execution by imports anywhere in running code, package initializers, star
imports, package re-exports, and names bound differently in
tryorifbranches; @overloadsignatures with their implementation, single-underscore library hooks such as IPython's_repr_html_, and the methods of classes handed to code outside the project;- deterministic component findings with stable fingerprints;
- FastAPI applications, routers, nested
Depends, lifespan, selected hooks, background tasks, Pydantic hooks, and a boundedcreate_apppattern; - Dishka
@provide, class providers, aliases,from_context,FastapiProvider,@inject,DishkaRoute, multiple applications/containers, and generator cleanup chains; - a separate pytest world that resolves fixtures, parametrization,
patcharguments,pytest_plugins, imported fixtures, hooks, inherited test methods, and the configuredpython_files,python_classes, andpython_functionsthe way pytest does; - conservative property/protocol/metaclass hooks and historical Django
RunPythonretention; - Django settings as application roots: installed applications'
models,admin, template tags, management commands, andAppConfig, the ASGI and WSGI modules, relocated migrations, and the modules settings name by string; nestedMetaand PydanticConfigclasses; Alembic environments and revisions; - PEP 621, Poetry, flit, and
setup.cfgconsole, GUI, and plugin entry points as statically read, provenanced production roots, including values such as a Typer application, and the hooks and fixtures ofpytest11plugins; Typer and Click commands; - automatic worlds when none are configured: scripts with a main guard and
__main__modules, applications of Flask, Celery, Starlette, Litestar, aiohttp, Sanic, Quart, Falcon, Bottle, Typer, and FastStream, taskiq schedulers, arq workers, database migrations, uncalled FastAPI factories, Celery task autodiscovery, programs that Compose files, Dockerfiles,Procfiles, Makefiles, and shell scripts start, classes that configuration files name, and otherwise a library's public API; - explicit external keep contracts, saved explanations, baselines, report comparison, and CI exit policies;
- schema-1 JSON, text output, compatibility guards, and a performance benchmark artifact.
Unknown assembly, conditional activation, Dishka components/decorators, unresolved plugins, and untested pinned framework versions weaken the affected world instead of generating stronger negative findings.
Source discovery skips virtual environments, site-packages, node_modules, and hidden
directories, and names what it skipped. When a scan reports little because unknown
boundaries protect most code, the report names the widest ones and where they are.
Install
Deadtrace runs on CPython 3.12, 3.13, and 3.14. It never imports the project it scans, so the
interpreter that runs it only needs to be new enough to parse that project's syntax (a file with newer
syntax is reported as DT1001). With uv:
uv tool install --python 3.12 "deadtrace==0.1.0a3"
deadtrace --version
deadtrace scan .
Or install into a Python 3.12 virtual environment:
python -m pip install "deadtrace==0.1.0a3"
For development from this repository:
uv sync --locked --all-groups
uv run deadtrace --help
To test the distributable package without importing the source tree:
uv build
uv run --isolated --no-project --python 3.12 \
--with ./dist/deadtrace-0.1.0a3-py3-none-any.whl deadtrace --version
The interpreter is selected explicitly because --no-project ignores .python-version, and the
default python on PATH may be older than 3.12.
Exercise scanning and saved artifacts from the installed wheel with:
uv run --isolated --no-project --python 3.12 \
--with ./dist/deadtrace-0.1.0a3-py3-none-any.whl python scripts/wheel_smoke.py
The smoke script uses a temporary directory, rejects a source-tree package import, and checks inventory, analysis, determinism, explanations, baselines, comparisons, and no target execution.
Quickstart: FastAPI + Dishka
The reference application contains a live endpoint → service → repository path and one registered but unrequested legacy binding:
uv run deadtrace scan corpus/reference/fastapi_dishka_basic
uv run deadtrace scan corpus/reference/fastapi_dishka_basic \
--format json --output deadtrace-report.json --require-complete
uv run deadtrace explain RCH003-FINGERPRINT --report deadtrace-report.json
Use the exact fingerprint printed by the scan. explain reads only the saved report, so changing
the project cannot silently change the explanation.
Configuration
[tool.deadtrace]
require-complete = true
exclude = ["testdata/**"] # data the project never runs; excluded code protects nothing
report-exclude = ["generated/**"] # affects presentation, never the source universe
max-steps = 100000 # optional; by default the budget is sized from the graph
[[tool.deadtrace.worlds]]
profile = "production"
scenario = "web"
roots = ["app.main:app"]
frameworks = ["python", "fastapi", "dishka", "pydantic"]
[[tool.deadtrace.keep]]
target = "app.plugins:external_hook"
reason = "Loaded by the deployment platform"
profiles = ["production"]
A world's frameworks list is declarative (ADR-0014): it records the frameworks the world is
expected to use, while every capability applies to every world, since switching one off could
only remove protection. Unknown names are rejected.
External keeps propagate conservatively through the callback body. They do not become fabricated resolved calls and are not equivalent to hiding a warning.
Findings and exit codes
| Code | Meaning |
|---|---|
RCH001 |
Unreached semantic component in complete production worlds |
RCH002 |
Route on a router not published by a modeled application |
RCH003 |
Registered Dishka binding with no established demand |
RCH004 |
Production code reached only from a complete pytest world |
Exit code 0 means the command completed; limitations may still be present. 1 is emitted only by
an explicitly selected finding policy (--fail-on-findings, --fail-on-new). 2 means an
operational error or a failed completeness/comparability requirement, and takes priority over 1.
Blocked findings
Some forms of finding were mostly false when public projects were reviewed by hand, so they are
held back by default rather than shown as dead code. A blocked finding keeps its code and
fingerprint; the JSON report lists it under blocked_findings with its reasons, and
--show-blocked prints it in text output. Exit codes, baselines, and comparisons use only the
findings that are shown.
| Reason | What it covers |
|---|---|
external_base_hooks |
A method of a class whose base the project does not define, which frameworks call by name |
non_production_area |
Code under examples, docs, scripts, tools, benchmarks, demo, samples |
public_api |
A public name of a public package module, when the project documents its API or lists the name in __all__ |
mentioned_in_config |
A name that a YAML, TOML, INI, template, Makefile, or Dockerfile of the project mentions |
RCH002 and RCH003 come from a framework model and are never blocked.
Baseline and report comparison
A baseline records reviewed debt without removing findings from the report or changing the graph:
uv run deadtrace baseline create deadtrace-report.json \
--output deadtrace-baseline.json --reason "reviewed before adoption"
uv run deadtrace scan . --format json --baseline deadtrace-baseline.json --fail-on-new
Refreshing a baseline keeps only exact reviewed entries and does not accept new findings unless that decision is explicit:
uv run deadtrace baseline update deadtrace-baseline.json deadtrace-report-new.json \
--output deadtrace-baseline-refreshed.json
uv run deadtrace baseline update deadtrace-baseline.json deadtrace-report-new.json \
--output deadtrace-baseline-reviewed.json --accept-new --reason "reviewed new debt"
Compare two reports directly; Git access is not required:
uv run deadtrace compare before.json after.json --fail-on-new --require-comparable
Changing the model revision, configuration, capabilities, or the support of target dependency
versions produces partially_comparable, not a misleading “all findings resolved” result.
Reports without a common execution world are incomparable; incomplete inputs can never be
comparable. Added tests, routes, scripts, and applications are source changes: added and
removed source files, worlds, and roots are listed separately from finding changes, and a
baseline stays comparable across them. Fingerprints depend on a finding's code and members only.
When a new model revision makes a baseline incomparable, baseline update carries reviewed
entries over by code and members.
Create an inspectable troubleshooting artifact without source text, file paths, symbol names, configuration values, environment variables, or object values:
uv run deadtrace support-bundle .
uv run deadtrace support-bundle . --output deadtrace-support.json
When --output is used, Deadtrace prints a privacy/content preview before writing the bundle.
GitHub Action and pre-commit
A composite action in this repository runs a scan in a workflow. It installs an exact PyPI version with uv, so the analyzed project is not imported:
jobs:
dead-code:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: kiselas/deadtrace@v0.1.0a3
with:
baseline: deadtrace-baseline.json
Inputs: path (default .), baseline (fail only on findings it does not accept),
fail-on-findings, version (an exact release, default the tag's own), and python-version
(default 3.12). Pin the action to a release tag.
For pre-commit, the repository publishes two hooks, deadtrace (needs deadtrace-baseline.json)
and deadtrace-report (fails on any finding):
repos:
- repo: https://github.com/kiselas/deadtrace
rev: v0.1.0a3
hooks:
- id: deadtrace
language_version: python3.12
Supported and guarded behavior
The oracle environment currently pins FastAPI 0.141.1, Dishka 1.10.1, and HTTPX 0.28.1. The
modeled subset is supported for FastAPI >=0.100,<1 and Dishka >=1.0,<2 (ADR-0018): an exact,
different version found in uv.lock or pyproject.toml inside the range is reported as DT4002,
and one outside it creates DT4001 and blocks strong negative findings. An unpinned dependency is
not presented as version-verified.
A pattern for which a complete world reports code that may run as unreached is a contract
violation. It is recorded under fixtures/known-violations
before it is fixed. Recorded cases, including receiver-flow violations across branches and loop
iterations fixed in model revision 23, now live in corpus/. A method whose name is also used on a value of unknown type is protected
rather than reported, and a function under an unmodeled framework's decorator is protected in
any world that loads its module. The methods of a class with an external base, other than
builtins, abc, typing, pydantic.BaseModel, and dishka.Provider, are protected once the
class is used, and so are the methods of a class handed to code outside the project.
Dishka conditional activation and components/decorators are detected but guarded. Dynamic imports,
unrestricted reflection, implicit descriptors/metaclasses, arbitrary pytest plugins, and general
Python dispatch are not claimed as fully modeled; known project hooks are protected transitively.
Static Django RunPython callbacks are a safety-only retention capability, not Django application
support. Dynamic package entry points are not executed; they emit DT4102, while explicit worlds
remain a deterministic override. Run deadtrace doctor PATH to see the capability and limitation
state for each world.
Corpus and development
uv run deadtrace cases validate fixtures/cases
uv run deadtrace cases validate corpus
uv run ruff check .
uv run ruff format --check .
uv run mypy
uv run pytest --cov
uv run deadtrace benchmark corpus/reference/fastapi_dishka_basic --runs 5
uv build
corpus/ contains executable semantic projects with independent expectations for worlds, findings,
limitations, and target states. Oracle tests execute only the trusted included reference project;
ordinary scan never imports or executes target modules and does not use the network.
Project documentation
The canonical implementation plan is ROADMAP.md. Architecture decision records are in docs/adr. The analysis contract, architecture, support matrix, and release acceptance define this alpha's scope and verification. Repository policies are in CONTRIBUTING.md, SECURITY.md, GOVERNANCE.md, and CODE_OF_CONDUCT.md.
The 2026-10-01 repository audit and improvement plan records current verification and remaining priorities. Methodology is a reviewed research proposal; it does not change the active milestone or authorize future target execution.
License
Apache License 2.0. See LICENSE.
Metadata
Release files for deadtrace 0.1.0a3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| deadtrace-0.1.0a3.tar.gz | 649.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| deadtrace-0.1.0a3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 819.0 kB
Release files / deadtrace-0.1.0a3.tar.gz
| Download URL | deadtrace-0.1.0a3.tar.gz |
|---|---|
| Size | 649.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
14de24d92af7826cccd07a7ea2935ffe448c2d18f5f2685ba2cfe402b3137615
|
|
BLAKE2b-256 checksum How to use checksums |
5a2b6408970602372e4ddaed1015425ea86be5b7a65414f3a783ee5fb85acc20
|
| 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 12, 2026.
Transparency logRelease files / deadtrace-0.1.0a3-py3-none-any.whl
| Download URL | deadtrace-0.1.0a3-py3-none-any.whl |
|---|---|
| Size | 169.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fdd034ca08c64e15c5132c70f8b4236bf3f1eacb1d8800da02ed3f60ebfea57d
|
|
BLAKE2b-256 checksum How to use checksums |
8d5ccea4348088429709278fb9f388cc737c02419fa89d209e26ec57e36702d6
|
| 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 12, 2026.
Transparency log