← Back to list

Understanding How Mypy Maps Files to Module Names

One of the most confusing aspects of mypy is that it doesn’t just type-check files. Internally, mypy builds a graph of Python modules. To…

keisuke nebashi · 2026-06-08 07:50 · 0 claps · 3.9 min read
#python #mypy
Open on Medium ↗

Understanding How Mypy Maps Files to Module Names

One of the most confusing aspects of mypy is that it doesn’t just type-check files. Internally, mypy builds a graph of Python modules. To do that, it must answer two questions:

  1. Which files should be checked?
  2. What is the fully-qualified module name of each file?

Many import-related errors stem from misunderstanding the second question.

The Two Phases

Mypy operates in roughly two phases:

Phase 1: Discover source files

For example:

mypy -m glue.script.ml.split

or:

mypy -p glue.script

or:

mypy glue/script/ml/split.py

Depending on the command, mypy discovers one or more source files.

Verbose mode reveals this step:

LOG: Found source:
BuildSource(path='...', module='...')

For example:

LOG: Found source:
BuildSource(
    path='/workspace/dmp/glue/script/ml/split.py',
    module='glue.script.ml.split'
)

Phase 2: Resolve imports

After loading source files, mypy follows imports:

import common

or:

from glue.script import common

This phase uses MYPYPATH, mypy_path, the current working directory, installed packages, and typeshed.

A common mistake is assuming MYPYPATH only affects import resolution. In practice, MYPYPATH can also affect how files are mapped to module names.

Three Ways to Run Mypy

Method 1: Passing a file path

mypy glue/script/ml/split.py

Here mypy starts with a file path and must determine the module name.

This is where package discovery matters.

Method 2: Passing a module

mypy -m glue.script.ml.split

Here the module name is already known.

Mypy simply locates the file corresponding to:

glue.script.ml.split

No path-to-module conversion is required.

Method 3: Passing a package

mypy -p glue.script

Again, the package name is already known.

Mypy recursively discovers modules underneath that package.

How Mypy Converts Paths Into Module Names

Consider:

/workspace/dmp/
└── glue/
    └── script/
        ├── common.py
        └── ml/
            └── split.py

Suppose:

MYPYPATH=/workspace/dmp

and:

mypy --explicit-package-bases glue/script/ml/split.py

Mypy looks for the nearest package base.

It finds:

/workspace/dmp

The relative path is:

glue/script/ml/split.py

Mypy converts that to:

glue.script.ml.split

by:

  1. Removing .py
  2. Replacing / with .
  3. Using the path relative to the package base

Result:

glue.script.ml.split

Why Multiple Package Bases Cause Problems

Suppose:

/workspace/dmp/
└── glue/
    └── script/
        ├── common.py
        └── ml/
            └── split.py

Suppose we run:

MYPYPATH=/workspace/dmp/glue/script \
mypy \
  --module glue.script.ml.split \
  --module glue.script.common

and split.py contains:

import common

Mypy reports:

Source file found twice under different module names:
"glue.script.common" and "common"

Why?

How Mypy Thinks About Files

Internally, mypy does not primarily operate on files.

Instead, it builds a dependency graph of modules:

module name -> source file

For example:

glue.script.common
    -> /workspace/dmp/glue/script/common.py
glue.script.ml.split
    -> /workspace/dmp/glue/script/ml/split.py

This mapping is expected to be one-to-one.

A single file should have exactly one canonical module name.

Step 1: Explicitly Checked Modules

The command line specifies:

--module glue.script.ml.split
--module glue.script.common

As a result, mypy starts with:

glue.script.ml.split
glue.script.common

Verbose logging confirms this:

Found source:
BuildSource(path=None, module='glue.script.ml.split')
Found source:
BuildSource(path=None, module='glue.script.common')

At this point there is no conflict.

Mypy knows:

glue.script.common
    -> common.py

and proceeds with type checking.

Step 2: Import Resolution

Next, mypy analyzes split.py.

Inside the file it encounters:

import common

This statement is important.

Python semantics are straightforward:

import common

means:

Import a module literally named "common"

It does not mean:

Import glue.script.common

Mypy therefore begins searching for a module named:

common

Step 3: MYPYPATH Makes the Import Succeed

The environment contains:

MYPYPATH=/workspace/dmp/glue/script

Within that directory exists:

common.py

Therefore mypy successfully resolves:

common
    -> /workspace/dmp/glue/script/common.py

Step 4: The Collision

At this point mypy knows:

glue.script.common
    -> /workspace/dmp/glue/script/common.py

and also:

common
    -> /workspace/dmp/glue/script/common.py

The same file now appears twice in the dependency graph:

glue.script.common
    -> common.py
common
    -> common.py

Mypy rejects this situation and raises:

Source file found twice under different module names:
"glue.script.common" and "common"

Why Multiple Package Bases Matter

A common explanation is:

“Multiple package bases cause duplicate modules.”

This is not entirely accurate.

Multiple package bases merely make duplicate module names possible.

The actual error occurs only when all of the following conditions hold:

Condition 1

The file is already known under one module name:

glue.script.common

Condition 2

Some import statement requests a different module name:

import common

Condition 3

Search paths allow both names to resolve to the same file.

Only when all three conditions are satisfied does mypy detect a duplicate module identity.

Reading the Verbose Log

Verbose output reveals exactly what happened:

LOG: Metadata fresh for glue.script.common:
     file /workspace/dmp/glue/script/common.py

Later:

LOG: Metadata fresh for common:
     file /workspace/dmp/glue/script/common.py

The second line is the key clue.

It proves that mypy loaded the same file under another module name.

Once these two lines appear together, the duplicate-module error is inevitable.

Two Consistent Solutions

Option 1: Use Package Imports

Treat the project as a package:

from glue.script import common

or:

from .. import common

Use:

MYPYPATH=/workspace/dmp

or no MYPYPATH at all.

Now the file has only one identity:

glue.script.common

Option 2: Use Top-Level Imports

Keep:

import common

and use:

MYPYPATH=/workspace/dmp/glue/script

Treat glue/script as the source root.

In this model, the canonical module name becomes:

common

rather than:

glue.script.common

Key Takeaway

The error message is easy to misinterpret.

Mypy is not complaining that it cannot find a file.

In fact, it found the file successfully.

Twice.

The real problem is that mypy’s dependency graph requires a single module identity per file. When one file can be reached as both:

glue.script.common

and:

common

mypy refuses to guess which identity is correct and aborts with a duplicate-module error.

Why import common Matters

Suppose split.py contains:

import common

That statement means exactly:

common

It does not mean:

glue.script.common

Even if common.py physically lives under:

glue/script/common.py

Python and mypy will not automatically reinterpret the import.

Therefore:

import common

requires mypy to locate a module literally named:

common

If the same file is already known as:

glue.script.common

a duplicate-module error becomes likely.

A Useful Mental Model

Think of mypy as maintaining a mapping:

module name -> file

Good:

glue.script.common -> common.py
glue.script.ml.split -> split.py

Bad:

glue.script.common -> common.py
common -> common.py

The second mapping assigns two module names to the same file, which mypy rejects.

Debugging Tips

Enable verbose logging:

mypy -v ...

Look for:

Found source:
BuildSource(...)

to see how mypy initially identifies modules.

Then search for:

Metadata fresh for ...

to see every module name assigned during dependency resolution.

If the same file appears under multiple module names, you’ve found the source of a duplicate-module error.


메타데이터
post_id
bc72df5313c3
slug
understanding-how-mypy-maps-files-to-module-names-bc72df5313c3
url
https://medium.com/@nebakei.tkb713/understanding-how-mypy-maps-files-to-module-names-bc72df5313c3
canonical_url
https://medium.com/@nebakei.tkb713/understanding-how-mypy-maps-files-to-module-names-bc72df5313c3
author_url
https://medium.com/@nebakei.tkb713
status
ok
fetched_at
2026-06-20 20:29:01