A NoneType AttributeError inside dataclasses means your importlib-loaded module is missing from sys.modules

I imported a Python file by its path with the usual three-line importlib
recipe, and it blew up inside the standard library’s dataclasses.py with
AttributeError: 'NoneType' object has no attribute '__dict__'. The error
names neither importlib nor sys.modules, and the exact same recipe had
worked on other files for months.

TL;DR — add sys.modules[spec.name] = mod before calling
spec.loader.exec_module(mod). Without it, any module that combines
from __future__ import annotations with a @dataclass crashes at import,
because dataclasses resolves string annotations through
sys.modules.get(cls.__module__), which returns None for an unregistered
module.

The setup

I was writing a test harness for a single-file log analyzer: one .py
script, standard library only, that parses web-server access logs into
@dataclass(slots=True) records. The script is not a package and not on
sys.path, so the harness imported it by file path with the short recipe
that circulates everywhere:

import importlib.util

spec = importlib.util.spec_from_file_location("plugin", "/path/to/plugin.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)   # crashes here

The script’s first line is from __future__ import annotations, which turns
every annotation in the file into a plain string at runtime. That line is the
whole story.

What I expected

exec_module runs the file’s code with the module’s namespace as globals.
I expected that to be equivalent to a normal import, minus the sys.path
search — the module object exists, its __name__ is set, the code runs
inside it. Registration in sys.modules looked like an optional courtesy
for circular imports and pickling, not something the file’s own top-level
code could depend on.

What actually happens

A minimal file is enough to reproduce it:

from __future__ import annotations
from dataclasses import dataclass

@dataclass
class P:
    x: int

Loading that file with the three-line recipe on Python 3.14.4 crashed with a
traceback that points at the decorator, then descends into the standard
library:

  File "/path/to/plugin.py", line 4, in <module>
    @dataclass
     ^^^^^^^^^
  File "/usr/lib/python3.14/dataclasses.py", line 1055, in _process_class
    and _is_type(type, cls, dataclasses, dataclasses.KW_ONLY,
  File "/usr/lib/python3.14/dataclasses.py", line 814, in _is_type
    ns = sys.modules.get(cls.__module__).__dict__
AttributeError: 'NoneType' object has no attribute '__dict__'. Did you mean: '__dir__'?

Two control experiments pinned it down. The same file with the
from __future__ import annotations line removed loaded fine, unregistered.
And the original file loaded fine once I registered the module first. The
real analyzer script behaved identically to the toy file: unregistered
import crashed at its first dataclass, registered import succeeded.

Lookup chain from a dataclass field with a string annotation through dataclasses._is_type to sys.modules: unregistered, the by-name lookup returns None and .__dict__ raises AttributeError; registered first, the same lookup finds the module and the class builds.

The fix

One line, placed before exec_module:

import importlib.util
import sys

def import_from_path(module_name, file_path):
    spec = importlib.util.spec_from_file_location(module_name, file_path)
    mod = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = mod          # the line that matters
    spec.loader.exec_module(mod)
    return mod

plugin = import_from_path("plugin", "/path/to/plugin.py")
print(plugin.P(1))   # P(x=1)

This is in fact what the current official recipe
does — the sys.modules line is there, uncommented and unexplained. The
three-line version without it survives in older answers and in muscle
memory, and it genuinely works for most files, which is what makes the
failure look random. Pick a module_name that cannot collide with a real
installed module, and if the load can fail, remove the entry again in an
except block so a half-executed module does not linger in sys.modules.

Why it works

With from __future__ import annotations, the annotation on x: int is the
string "int", not the type int. The @dataclass decorator still has to
find out whether each annotation is one of its own markers — ClassVar,
InitVar, or KW_ONLY — because those change how a field is treated. With
real objects it can compare identities. With strings it can only match
text, and to guard against KW_ONLY meaning something else in your module,
it looks the identifier up in the defining module’s globals:

# dataclasses.py, _is_type
ns = sys.modules.get(cls.__module__).__dict__

A normal import statement registers the module in sys.modules before
executing its body — that is precisely what makes circular imports
resolvable. importlib.util.module_from_spec does not; registration is left
to the caller. So during exec_module, cls.__module__ is set but
sys.modules.get() returns None, and the .__dict__ access produces an
error with no importlib frame in it.

Without the __future__ import there is no string to resolve, _is_type is
never consulted, and the unregistered recipe works — which is why the recipe
can sit in a codebase for a long time before one loaded file opts into
string annotations and breaks it.

What I did not test

Only Python 3.14.4 on Linux. On 3.14, annotations are lazily evaluated by
default (PEP 649), and my unregistered import of a future-free dataclass
module worked; older versions evaluate annotations eagerly and I did not
rerun the matrix there, though _is_type has resolved strings through
sys.modules for a long line of releases. I also did not test other
consumers of cls.__module__typing.get_type_hints, pickling, inspect
— which plausibly fail in their own ways on an unregistered module.

Facts

context: importing a .py file by path with importlib (spec_from_file_location, module_from_spec, exec_module)
problem: module using `from __future__ import annotations` plus @dataclass crashes with AttributeError NoneType __dict__ in dataclasses._is_type
solution: register the module first — sys.modules[spec.name] = mod — then call exec_module
verified_on: 2026-08-17
applies_to: [Python 3.14.4, dataclasses with string annotations, CPython]
does_not_apply_to: [modules without `from __future__ import annotations`, normal `import` statements]

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Retour en haut