Python modulefinder analyzes a script and attempts to determine which modules it imports. The result can support dependency audits, packaging preparation, missing-import diagnostics, and internal build reports.
It follows imports discovered while inspecting code. That is not the same as exercising every runtime path. Dynamically constructed imports, plugins discovered from configuration, calls to importlib.import_module(), and custom import hooks may not appear. Treat the report as useful evidence rather than a perfect inventory.
Create a first ModuleFinder report
The main class is modulefinder.ModuleFinder. Call run_script() with a Python file, then use report() for a human-readable summary.
from modulefinder import ModuleFinder
finder = ModuleFinder()
finder.run_script('application.py')
finder.report()The report includes discovered modules, paths, and modules that are missing or appear to be missing. For automated systems, access the internal mappings and emit structured JSON instead.
Read the modules mapping
finder.modules maps module names to analysis objects.
from modulefinder import ModuleFinder
finder = ModuleFinder()
finder.run_script('application.py')
for name, module in sorted(finder.modules.items()):
print(name, module.__file__)Built-in and internal modules may not have a physical file. Handle None as a valid value. To understand installation paths, see Python sysconfig.
Inspect missing modules
Potential problems appear in badmodules. They may be real failures, optional imports protected by try/except ImportError, platform-specific modules, or false positives.
for name in sorted(finder.badmodules):
print('Possibly missing:', name)Do not fail a build for every entry automatically. Classify required and optional dependencies, consider the target platform, and verify behavior in a clean environment.
Optional imports
Libraries often try to load accelerators, database backends, or operating-system integrations.
try:
import uvloop
except ImportError:
uvloop = NoneThe analyzer can report uvloop as missing even when the fallback is intentional. Maintain a documented allowlist of accepted optional modules rather than suppressing all warnings.
Customize search paths
The constructor accepts a path list. If omitted, it uses sys.path.
finder = ModuleFinder(path=[
'/project/src',
'/project/vendor',
])
finder.run_script('/project/src/app.py')Use absolute controlled paths. Blindly adding the current directory may cause local files to shadow legitimate dependencies.
Exclude known modules
The excludes argument skips selected names.
finder = ModuleFinder(
excludes=['tkinter', 'tests', 'devtools'],
)Exclusions can improve speed and remove known optional trees, but an oversized list hides real dependencies. Store a reason for each exclusion.
Normalize paths in reports
replace_paths accepts (old, new) pairs.
finder = ModuleFinder(
replace_paths=[
('/home/alex/project', '<PROJECT>'),
('/opt/build/venv', '<VENV>'),
]
)Normalization improves comparison between machines and prevents usernames or internal directory layouts from leaking into shared artifacts.
Add a package path
AddPackagePath() records an additional location for a package.
import modulefinder
modulefinder.AddPackagePath(
'my_plugin',
'/opt/plugins/my_plugin',
)Use this only for known layouts. If a path comes from configuration, resolve it and restrict it to an approved root.
Replace a module with a package
ReplacePackage(oldname, newname) tells the analyzer that one name should be treated as another package. This specialized compatibility feature can support unusual layouts.
Document every replacement. Reports become confusing when readers do not know that names were rewritten. Prefer fixing the package layout when you control the code.
Run modulefinder from the command line
The module file can also be executed as a script and given a Python filename. The API is usually better for pipelines because it allows filtering, normalization, and structured output.
Store the report as a build artifact and compare it between releases. An unexpected dependency may indicate architectural growth, an accidental import, or a packaging change.
Generate structured JSON
import json
from modulefinder import ModuleFinder
finder = ModuleFinder()
finder.run_script('app.py')
report = {
'modules': {
name: getattr(module, '__file__', None)
for name, module in sorted(finder.modules.items())
},
'missing': sorted(finder.badmodules),
}
with open('imports.json', 'w', encoding='utf-8') as stream:
json.dump(report, stream, indent=2)Normalize sensitive paths before writing. Cap output size for very large dependency graphs.
Compare releases
before = set(old_report['modules'])
after = set(new_report['modules'])
print('Added:', sorted(after - before))
print('Removed:', sorted(before - after))The difference reflects static reachability from the analyzed entry point. It does not prove that every module is used in every execution.
Dynamic imports are a blind spot
from importlib import import_module
name = settings['backend']
backend = import_module(f'my_app.backends.{name}')The final module name is not a literal import. Maintain an explicit plugin manifest or combine modulefinder with tests covering supported configurations. For packaged data, importlib.resources solves a different concern.
Entry points and plugin systems
Installed plugins can be discovered through distribution metadata without being imported in the main source. Modulefinder may never see them.
Query installed entry points and add expected packages to the dependency inventory. The standard importlib.metadata module is appropriate for this job.
Platform-conditional imports
import sys
if sys.platform == 'win32':
import winreg
else:
import pwdThe report depends on the environment used for analysis. Run it on each supported platform or maintain a documented matrix of conditional dependencies.
Virtual environments matter
The analyzer sees paths available to the current process. Run it in the same virtual environment used for testing or packaging.
Record the Python version, normalized sys.path, and installed dependency set. Without this context, reports from two machines may differ for reasons unrelated to source changes.
Use it before zipapp builds
Before creating a Python zipapp artifact, the report can suggest pure-Python dependencies that need to be copied into the staging directory.
Do not automatically copy every discovered file. Filter standard-library modules, native extensions, external paths, package data, and license requirements. The report is a starting point, not a complete packager.
Combine with compileall
Python compileall verifies syntax and builds bytecode; modulefinder maps imports. They address different failure modes.
A project can compile successfully while a required module is missing. It can also import correctly while an unreachable source file contains a syntax error.
Debug output
The debug argument increases diagnostic messages.
finder = ModuleFinder(debug=2)
finder.run_script('app.py')High debug levels may produce large logs containing local paths. Store them in protected locations and use short retention.
Performance and isolation
Large import graphs consume time and memory. Apply an external timeout, cap repository size, and run analysis in an isolated worker when processing third-party projects.
Although modulefinder does not normally run the full application, unknown source can still stress parsers or analysis logic. Avoid elevated privileges.
Test your reporting layer
Create small fixtures containing required, optional, relative, conditional, and dynamic imports.
def test_basic_dependency(tmp_path):
script = tmp_path / 'app.py'
script.write_text('import json\n', encoding='utf-8')
finder = ModuleFinder()
finder.run_script(str(script))
assert 'json' in finder.modulesFocus tests on project-relevant modules instead of asserting the entire standard-library graph, which can vary by Python build.
Common mistakes
- Treating the report as a perfect inventory.
- Ignoring dynamic imports and entry points.
- Failing for every optional missing module.
- Analyzing in a different environment than the build.
- Leaking local paths in reports.
- Copying every discovered module automatically.
- Testing only one platform.
Best practices
- Use reproducible environments.
- Normalize paths before storing results.
- Classify required and optional dependencies.
- Combine static analysis with runtime tests.
- Maintain manifests for dynamic plugins.
- Compare reports between releases.
- Isolate analysis of third-party code.
Conclusion
Python modulefinder reveals the import graph reachable from a script, including discovered paths and potentially missing modules. It is useful for audits, diagnostics, and packaging preparation.
Interpret the output with context. Dynamic imports, plugins, and platform conditions require additional checks. Consult the official modulefinder documentation and the Python import system reference.







