Requires Python 3.11+ (tested on 3.11–3.14).
pip install deadcodeTo see unused code findings:
deadcode .To see suggested fixes for all files:
deadcode . --fix --dryTo see suggested fixes only for foo.py file:
deadcode . --fix --dry foo.pyTo fix:
deadcode . --fixTune out some of the false positives, e.g.:
deadcode . --exclude=venv,tests --ignore-names=BaseTestCase,*Mixin --ignore-names-in-files=migrations
The same options can be provided in pyproject.toml settings file:
[tool.deadcode]
exclude = ["venv", "tests"]
ignore-names = ["BaseTestCase", "*Mixin"]
ignore-names-in-files = ["migrations"]
For FastAPI and Pydantic projects, suppress route handlers (matched by their decorators) and Pydantic models (matched by their base class):
[tool.deadcode]
ignore-definitions-if-decorated-with = ["@app.*", "@router.*"]
ignore-definitions-if-inherits-from = ["BaseModel"]
app and router are the conventional FastAPI instance names — rename the globs to match your own variables. TOML values are a list: each pattern is a separate list entry (they are not comma-split).
For SQLAlchemy and Alembic projects, suppress ORM models (matched by their base class), event listeners (matched by their decorator), and migration boilerplate (matched by name or by the migrations directory):
[tool.deadcode]
ignore-definitions-if-inherits-from = ["Base", "db.Model"]
ignore-bodies-if-inherits-from = ["Base", "db.Model"]
ignore-definitions-if-decorated-with = ["@event.*"]
ignore-names = ["upgrade", "downgrade", "revision", "down_revision", "branch_labels", "depends_on"]
ignore-names-in-files = ["*/versions/*"]
Base is the conventional SQLAlchemy declarative base; db.Model is the Flask-SQLAlchemy base — rename them to match your own. Note: a model that owns columns needs both ignore-definitions-if-inherits-from (silences the class name) and ignore-bodies-if-inherits-from (silences the Column / mapped_column members) — the definition option alone does not silence the columns.
For Django projects, suppress models (matched by their base class), views and app configs (matched by their base class), admin registrations and signals (matched by their decorators), and settings files (matched by their path):
[tool.deadcode]
ignore-definitions-if-inherits-from = ["models.Model", "View", "AppConfig", "admin.ModelAdmin", "BaseCommand"]
ignore-bodies-if-inherits-from = ["models.Model", "View", "AppConfig", "admin.ModelAdmin", "BaseCommand"]
ignore-definitions-if-decorated-with = ["@admin.register*", "@receiver*"]
ignore-names = ["urlpatterns"]
ignore-names-in-files = ["*settings*"]
models.Model is the Django ORM base class, View is the base for class-based views, AppConfig is the app configuration base, admin.ModelAdmin is the admin registration base, and BaseCommand is the management command base. Rename the globs to match your own. Settings files are suppressed by path pattern to avoid listing each constant individually. The urlpatterns list variable is suppressed by name since Django imports it dynamically.
| Option | Type | Meaning |
|---|---|---|
--fix |
- | Automatically remove detected unused code expressions from the code base. |
--dry |
- or list | Show changes which would be made in files. Shows changes for provided filenames or shows all changes if no filename is specified. |
--exclude |
list | Filenames (or path expressions), which will be completely skipped without being analysed. |
--ignore-names |
list | Removes provided list of names from the output. Regexp expressions to match multiple names can also be provided, e.g. *Mixin will match all classes ending with Mixin. |
--ignore-names-in-files |
list | Ignores unused names in files, which filenames match provided path expressions. |
--ignore-names-if-inherits-from |
list | Ignores names of classes, which inherit from provided class names. |
--ignore-names-if-decorated-with |
list | Ignores names of an expression, which is decorated with one of the provided decorator names. |
--ignore-bodies-of |
list | Ignores body of an expression if its name matches any of the provided names. |
--ignore-bodies-if-decorated-with |
list | Ignores body of an expression if its decorated with one of the provided decorator names. |
--ignore-bodies-if-inherits-from |
list | Ignores body of a class if it inherits from any of the provided class names. |
--ignore-definitions |
list | Ignores definition (including name and body) if a name of an expression matches any of the provided ones. |
--ignore-definitions-if-inherits-from |
list | Ignores definition (including name and body) of a class if it inherits from any of the provided class names. |
--ignore-definitions-if-decorated-with |
list | Ignores definition (including name and body) of an expression, which is decorated with any of the provided decorator names. |
--no-color |
- | Removes colors from the output. |
--count |
- | Provides the count of the detected unused names instead of printing them all out. |
--quiet |
- | Does not output anything. Makefile still fails with exit code 1 if unused names are found. |
name - variable, function or class name.
body - code block which follows after : in function or class definition.
definition - whole class or function definition expression including its name and body.
| Code | Name | Message |
|---|---|---|
| DC01 | unused-variable | Variable {name} is never used |
| DC02 | unused-function | Function {name} is never used |
| DC03 | unused-class | Class {name} is never used |
| DC04 | unused-method | Method {name} is never used |
| DC05 | unused-attribute | Attribute {name} is never used |
| DC06 | unused-name | Name {name} is never used |
| DC07 | unused-import | Import {name} is never used |
| DC08 | unused-property | Property {name} is never used |
| DC09 | unreachable-if-block | Unreachable conditional statement block |
| DC11 | empty-file | Empty Python file |
| DC12 | commented-out-code | Commented out code |
| DC13 | unreachable-code | Code after terminal statement, e.g. return, raise, continue, break |
| DC | ignore-expression | Do not show any findings for an expression, which starts on current line (this code can only be used in # noqa: DC comments) |
Inline # noqa comments can be used to ignore deadcode checks.
E.g. unused Foo class wont be detected/fixed because # noqa: DC03 comment is used:
class Foo: # noqa: DC03
passTo develop Deadcode, clone the repository and use Just to manage the development workflow:
# Install dependencies and Deadcode in development mode
just syncThis installs the development dependencies from requirements-dev.txt into the project environment and ensures Deadcode is available for development.
All code quality checks (tests, linting, formatting, type checking, complexity) are managed via Just targets:
# Run all checks (tests + lint + format check + type check + complexity check)
just check
# Run individual checks
just test # Run tests with coverage (parallel via pytest-xdist on all-but-one cores)
just format # Auto-format code with ruff
just lint # Check and auto-fix linting with ruff
just typecheck # Type check with mypy
just check-complexity # Report functions exceeding complexity limit (max 8)
# Check-only mode (no auto-fixes)
just check-format # Check formatting without changing files
just check-lint # Check linting without changing files (exit 0 if clean)
just check-typecheck # Check types without changing filesThe codebase is continuously kept ruff-clean (zero lint errors, zero warnings) and fully type-checked under mypy's strict mode. The just check gate ensures code quality before merging.
Apply automatic code fixes with:
just fix # Run format then fixlint (comprehensive fixes)This combines:
just format— auto-format code with ruffjust fixlint— auto-fix linting issues withruff check --fix --unsafe-fixesthenjust deadcode --fix
just deadcode # Run Deadcode on itself (unused-code detection)
just audit # Run pip-audit for security vulnerabilitiesThese are included in the broader check suite but can also be run independently.
just lock # Refresh uv.lock after dependency changes
just compile # Regenerate requirements.txt and requirements-dev.txtjust publish # Build and publish to PyPIThis cleans the dist/ directory, builds distribution packages, and publishes them.
All tests pass regardless of the current working directory, PYTHONHASHSEED, and xdist worker count. This is achieved through:
- Sorted directory walk — file discovery order is deterministic across filesystems
- Config mocking — unit tests are isolated from the repo's real
pyproject.tomlsettings - Isolated file systems — FS-bypass unit tests use temporary directories instead of cwd-relative literals
- Root anchoring — integration tests anchor to the repo root via
chdirwith cleanup handlers - Parallel execution —
just testruns parallel tests on all-but-one CPU cores via pytest-xdist
For details, see Testing & Tooling.
Test coverage is measured on the deadcode/ package only (excluding test infrastructure). The test suite maintains a 95.0% line coverage threshold enforced by pytest-cov. To check coverage:
# Run tests and display coverage report (includes term-missing for line-by-line breakdown)
just test
# Run coverage for a specific subset (e.g., actions module)
uv run pytest -o addopts="" --cov=deadcode --cov-report=term-missing tests/actions/Coverage configuration is in pyproject.toml:
- Source:
deadcode/package only (nottests/) - Omitted:
deadcode/utils/base_test_case.py(test infrastructure) - Gate: Enforced at
95.0%via--cov-fail-underinpytestoptions
The test suite includes dedicated coverage for Python 3.11+ and 3.12+ syntax features:
- 3.11-safe tests (
test_match_statement.py,test_walrus.py) run on all supported versions - 3.12+ tests (
test_type_alias.py,test_pep695_generics.py) are version-gated via@skipUnlessand only run on 3.12+
The suite also includes audit tests that verify detection correctness and pin known gaps:
- Confirmed-bug tests use
@unittest.expectedFailureto assert correct behavior while the implementation still produces the wrong result; when a fix lands, these tests flip to guards (unexpected passes) - By-design characterization tests (green, no
@expectedFailure) pin current behavior for intentionally limited features - Control tests accompany each "not reported" assertion to prove the visitor actually ran, preventing vacuous passes
See Development Documentation for full testing details and the audit testing pattern.
Use just check before submitting pull requests to ensure all checks pass:
- All tests pass with 95.0% coverage
- Code is ruff-clean (zero lint errors, zero warnings)
- Type checking passes with mypy in strict mode (zero type errors)
- Complexity limits are respected (maximum McCabe complexity of 8 per function)
See Development Documentation for the full development workflow.
ruff and
flake8 - don't have rules for unused global
code detection, only for local ones F823, F841, F842. deadcode package
tries to add new DCXXX checks for detecting variables/functions/classes/files
which are not used in a whole code base.
deadcode - is supposed to be used inline with other static code checkers like ruff.
There is an alternative vulture package.
In case there are several definitions using the same name - they all wont be reported if at least one usage of that name is being detected.
Files with syntax errors will be ignored, because deadcode uses ast to
build abstract syntax tree for name usage detection.
It is assumed that deadcode will be run using the same or higher Python version as the
code base is implemented in.
- Replace
.*with only*in regexp matching. - Add unused class method detection DC04 check.
- Add
--fixoption to automatically remove detected dead code occourencies - Add a check for empty python files.
- Split error codes into DC01, DC02, DC03 for variables, functions, class.
- Should have different codes for ignoring name and ignoring whole definition (reserved DCxx0 - ignore name, DCxx1 - ignore definition).
- Allow to disable each check separately using:
- inline comment.
- pyproject.toml file
- Add a check for code in comments.
- Add target python version option, if specified it will be used for code base check.
- Add a
--depthparameter to ignore nested code.. (To only check global scope use 0). - Add options:
- --ignore-definitions
- --ignore-definitions-if-inherits-from
- --ignore-definitions-if-decorated-with
- --ignore-names-if-inherits-from
- --ignore-names-if-decorated-with
- --ignore-bodies-of
- --ignore-bodies-if-decorated-with
- --ignore-bodies-if-inherits-from
- --ignore-definitions
- --ignore-definitions-if-inherits-from
- --ignore-definitions-if-decorated-with
- Question: would it be possible to ignore only certain types of checks for a body, e.g. only variable attributes of TypedDict and still check usage of methods and properties?
- What expression would allow this type of precission?
- Distinguish between definitions with same name, but different files.
- Repeated application of
deadcodetill the output stops changing. - Unreachable code detection and fixing: this should only be scoped for if statements and only limited to primitive variables.
-
--fix --dry [filenames]- only show whats about to change in the listed filenames. - Benchmarking performance with larger projects (time, CPU and memory consumption) in order to optimize.
-
--fixcould accept a list of filenames as well (only those files would be changed, but the summary could would be full). (This might be confusing, because filenames, which have to be considered are provided without any flag, --fix is expected to not accept arguments) - pre-commit-hook.
- language server.
- Use only two digits for error codes instead of 3. Two is plenty and it simplifies usage a bit
- DC10: remove code after terminal statements like
raise,return,break,continueand comes in the same scope. - Add
ignoreandper-file-ignorescommand line and pyproject.toml options, which allows to skip some rules. - Make sure that all rules are being skipped by
noqacomment and all rules react tonoqa: rule_idcomments. - Include package names into code item scope (dot-separated path), e.g. "package1.package2.module.class.method.variable".
- All options should be able to accept dot-separated path or a generic name, e.g. "marshmallow.Schema" vs "Schema", documentation should cleary demonstrate the behaviour/example that "Schema" means "*.Schema".
- Redefinition of an existing name makes previous name unreachable, unless it is assigned somehow.
- Check if file is still valid/parsable after automatic fixing, if not: halt the change and report error.
- v2.4.0:
- Fixed automatic fixing of imports, assignment statements.
- Started using
ast.unparse()in automatic code fixing. - Dropped Python3.8 support, since it does not support
ast.unparse()function, which is now used in automatic code fixing. - Added
--onlyoption, which now can be used with both--fixand--dryoptions. - Removed a file list option for
--dryoption, please use--onlyflag instead.
- v2.3.1:
- Added support for automatic removal of imports.
- v2.3.0:
- Add
--dryoption. - Update error codes to use DCXX format instead of DCXXX.
- Add
