sqlalchemy/alembic · error · ValueError
Duplicate table keys across multiple MetaData objects
Error message
Duplicate table keys across multiple MetaData objects: %s
What it means
The table_key_to_table aggregated property raises ValueError when two MetaData objects passed to configure() contain tables with identical keys (schema-qualified name). Alembic builds a single lookup dict across all metadata and cannot disambiguate duplicate keys, so it refuses rather than silently dropping one.
Solutions
- Rename one of the conflicting tables so each MetaData key is unique, or consolidate both into a single MetaData object.
- Use distinct schema names per MetaData so the keys (schema.table) differ.
- Pass only one MetaData to target_metadata, or deduplicate the list before configure().
- Inspect the reported keys in the error message to locate which metadata objects overlap.
Example fix
# before
context.configure(target_metadata=[meta_a, meta_b]) # both have 'users'
# after: single metadata
context.configure(target_metadata=meta_a)
# or give each a distinct schema
Table('users', meta_b, schema='billing') Defensive patterns
Strategy: validation
Validate before calling
from collections import Counter
def metadata_keys_overlap(metadata_list) -> list[str]:
keys = []
for m in metadata_list:
keys.extend(m.tables.keys())
return [k for k, c in Counter(keys).items() if c > 1]
conflicts = metadata_keys_overlap([meta_a, meta_b])
assert not conflicts, f'Duplicate table keys: {conflicts}' Type guard
from sqlalchemy import MetaData
def unique_table_keys(metadata_list: list[MetaData]) -> bool:
seen = set()
for m in metadata_list:
for k in m.tables:
if k in seen:
return False
seen.add(k)
return True Prevention
- Use a single MetaData for all models when possible.
- If multiple MetaData objects are required, assign distinct schemas so keys differ.
- Add a startup check that scans all metadata for key collisions.
When it happens
Trigger: Passing target_metadata=[metadata_a, metadata_b] to context.configure() where both metadata objects define a table named 'users' (or same schema.table). Also triggered by re-importing the same MetaData instance under different names in a list, or model packages that redefine shared tables.
Common situations: Multi-database or modular apps that register models into separate MetaData objects but share table names; accidental duplicate imports of model modules; merging two SQLAlchemy model bases that both declare a base table.
Related errors
- can't return inspector as this AutogenContext has no…
- A plugin named is already registered
- Autogenerate rendering of SQL Expression language…
- Can only return single object for DowngradeOps traverse
- Can only return single object for UpgradeOps traverse
AI-assisted analysis of sqlalchemy/alembic@5551b5d35f (2026-08-11).
Data as JSON: /api/errors/b0d2938a8131b6a9.
Report an issue: GitHub.
Appendix: source
Thrown at alembic/autogenerate/api.py:513
return result
@util.memoized_property
def table_key_to_table(self) -> dict[str, Table]:
"""Return an aggregate of the :attr:`.MetaData.tables` dictionaries.
The :attr:`.MetaData.tables` collection is a dictionary of table key
to :class:`.Table`; this method aggregates the dictionary across
multiple :class:`.MetaData` objects into one dictionary.
Duplicate table keys are **not** supported; if two :class:`.MetaData`
objects contain the same table key, an exception is raised.
"""
result: dict[str, Table] = {}
for m in util.to_list(self.metadata):
intersect = set(result).intersection(set(m.tables))
if intersect:
raise ValueError(
"Duplicate table keys across multiple "
"MetaData objects: %s"
% (", ".join('"%s"' % key for key in sorted(intersect)))
)
result.update(m.tables)
return result
class RevisionContext:
"""Maintains configuration and state that's specific to a revision
file generation operation."""
generated_revisions: list[MigrationScript]
process_revision_directives: ProcessRevisionDirectiveFn | None
def __init__(
self,View on GitHub (pinned to 5551b5d35f)