Fixing "relationship will copy column" conflict warnings
The warning means two relationships both maintain the same foreign key column: link them with back_populates if they are two ends of one relationship, mark the read-only one viewonly=True if one is just a view, and use overlaps="..." only when the columns really are shared and exactly one relationship writes them. This guide belongs to modeling relationships, cascades and association objects.
Quick Answer
The message is long, and every part of it is useful:
SAWarning: relationship 'Order.lines' will copy column orders.id to column order_lines.order_id,
which conflicts with relationship(s): 'OrderLine.order' (copies orders.id to order_lines.order_id).
If this is not the intention, consider if these relationships should be linked with
back_populates, or if viewonly=True should be applied to one or more if they are read-only.
Before — two halves of one relationship, declared as if they were two:
from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
lines: Mapped[list["OrderLine"]] = relationship() # writes order_lines.order_id
class OrderLine(Base):
__tablename__ = "order_lines"
id: Mapped[int] = mapped_column(primary_key=True)
order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"))
order: Mapped[Order] = relationship() # writes it too
After — one relationship with two views of it:
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
lines: Mapped[list["OrderLine"]] = relationship(back_populates="order")
class OrderLine(Base):
__tablename__ = "order_lines"
id: Mapped[int] = mapped_column(primary_key=True)
order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"))
order: Mapped[Order] = relationship(back_populates="lines")
back_populates does two things at once: it silences the warning, because there is now one relationship rather than two, and it keeps the sides synchronised in memory, so order.lines.append(line) also sets line.order without a database round trip.
Execution Context & Async Workflow Integration
A relationship() is not only a way to read related objects. It is also a persistence rule: at flush time, the unit of work copies the primary key of the parent into the foreign key of the child, for every relationship that says it owns that column. The warning is SQLAlchemy noticing, while it configures the mappers, that two rules claim the same column.
That is emitted at configuration time — the first time mappers are set up, typically on the first query or the first configure_mappers() — not at flush. So the warning appears long before any damage, in a place with no obvious connection to the code that will eventually misbehave. The damage, when it comes, is that two independent rules write the same column and the last write wins, with "last" decided by the unit of work's dependency sorting rather than by your code. Two attributes that should describe one link can then disagree in memory, and the row can end up with either value.
Three shapes produce it.
Two halves of one relationship, unlinked. The most common by far. Both sides genuinely describe one link, and back_populates says so. (backref did the same thing by generating the other side implicitly; 2.0 style prefers back_populates, because both ends are then visible where they are declared, and because type checkers can see them — see typing relationships with Mappedlist.)
A read-only second view. A secondary relationship kept for convenience alongside a mapped association object writes the same link table columns the association object writes. Neither is wrong; only one should persist. viewonly=True on the convenience one resolves it, as described in using association objects for many-to-many with extra columns.
Genuinely overlapping columns. Composite foreign keys where two relationships share a column — a tenant-scoped model where tenant_id participates in several keys is the classic case. Here the overlap is real and intended, and overlaps="..." records that decision.
Under async none of this behaves differently, but it is noticed later: warnings surface during mapper configuration inside the first awaited query, where a suppressed warning filter or a busy log makes them easy to miss. Running the test suite with -W error::sqlalchemy.exc.SAWarning turns them into failures, which is the only reliable way to keep them from accumulating.
Resolving Warnings, Errors & Common Mistakes
| Warning or symptom | Root Cause | Production Fix |
|---|---|---|
relationship 'Order.lines' will copy ... conflicts with 'OrderLine.order' | Both sides declared without back_populates. | Add back_populates to both. |
... conflicts with 'Order.products' where products uses secondary | A convenience relationship alongside an association object. | viewonly=True on the secondary relationship. |
| The warning names relationships on different classes sharing one column | Overlapping composite foreign keys. | overlaps="other_rel,another_rel" on each, after confirming one writer. |
| Changes to one side are not visible on the other until a refresh | Two unlinked relationships, so nothing synchronises in memory. | back_populates. |
ArgumentError: relationship X will copy column ... but there is no foreign key | foreign_keys or primaryjoin not set where the join is ambiguous. | Name them explicitly. |
| The warning disappeared but a column is still written unexpectedly | overlaps was used to silence a real conflict. | Make the non-writing relationship viewonly=True instead. |
| Warning appears only in production | Warnings filtered in the test configuration. | Run tests with -W error::sqlalchemy.exc.SAWarning. |
The distinction between overlaps and viewonly is the one worth getting right, because they look interchangeable and are not. overlaps is an assertion: it tells SQLAlchemy "I know these relationships share columns, and I have made sure that is fine." Nothing changes about how either behaves. viewonly is a change: the relationship no longer participates in persistence at all, so there is genuinely only one writer left.
from sqlalchemy.orm import relationship
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
items: Mapped[list["OrderItem"]] = relationship(
back_populates="order", cascade="all, delete-orphan"
)
# A convenience view across the association object. It must not persist anything.
products: Mapped[list["Product"]] = relationship(
secondary="order_items", viewonly=True
)
A viewonly relationship that code tries to write to fails quietly — appends are ignored at flush — so it is worth a comment where it is declared, and a test asserting the real write path still works. SQLAlchemy 2.0 also emits a separate warning when a viewonly collection is mutated, which makes the mistake visible during development rather than in production.
Advanced: Overlapping Foreign Keys in Tenant-Scoped Models
The one case where overlaps is the right answer is a schema in which a column legitimately participates in more than one relationship — most often a tenant_id that is part of every composite key, so that a foreign key can never cross a tenant boundary.
from sqlalchemy import ForeignKeyConstraint, PrimaryKeyConstraint
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class Order(Base):
__tablename__ = "orders"
__table_args__ = (PrimaryKeyConstraint("tenant_id", "id"),)
tenant_id: Mapped[int]
id: Mapped[int]
customer_id: Mapped[int]
customer: Mapped["Customer"] = relationship(
back_populates="orders",
# tenant_id is shared with the lines relationship below.
overlaps="lines",
)
lines: Mapped[list["OrderLine"]] = relationship(
back_populates="order", cascade="all, delete-orphan", overlaps="customer"
)
class OrderLine(Base):
__tablename__ = "order_lines"
__table_args__ = (
PrimaryKeyConstraint("tenant_id", "id"),
ForeignKeyConstraint(["tenant_id", "order_id"], ["orders.tenant_id", "orders.id"]),
)
tenant_id: Mapped[int]
id: Mapped[int]
order_id: Mapped[int]
order: Mapped[Order] = relationship(back_populates="lines", overlaps="customer")
Both relationships write order_lines.tenant_id — one as part of the link to the order, one as part of the link to the customer — and both write the same value, because a line's tenant is the order's tenant is the customer's tenant. The overlap is real, the outcome is well defined, and overlaps records that this was checked rather than overlooked.
Before adding it, verify the claim rather than assuming it. Two questions decide: can the two relationships ever write different values into the shared column, and does the schema prevent that anyway? Here a composite foreign key spanning (tenant_id, order_id) makes a cross-tenant link impossible at the database level, which is what justifies the assertion.
If the answer to the first question is yes — the two relationships could disagree — overlaps is the wrong tool, and the model needs a real decision about which relationship owns the column. Making the other one viewonly=True is usually that decision.
Composite keys like these interact with everything else in this section: cascades still work, association objects still work, and loader options are unchanged. What does change is that every relationship() needs enough information to build its join, so primaryjoin and foreign_keys appear more often than in a single-column schema — and the row-level security approach is often a simpler way to get the same isolation guarantee without putting tenant_id in every key.
Keeping Mapping Warnings From Accumulating
Mapping warnings share an unfortunate property: the code keeps working, so nothing forces anyone to look. A model with six of them is a model where the seventh — the one that is a real bug — goes unnoticed. Two habits keep the count at zero.
Configure mappers in a test, and fail on warnings. configure_mappers() forces SQLAlchemy to resolve every relationship in the registry, which is when these warnings are emitted. Calling it explicitly in a test means they surface even for models no test happens to query:
import warnings
import pytest
from sqlalchemy.exc import SAWarning
from sqlalchemy.orm import configure_mappers
import shop.models # noqa: F401 — importing registers every mapper
def test_mappers_configure_without_warnings():
with warnings.catch_warnings():
warnings.simplefilter("error", SAWarning)
configure_mappers()
That one test covers every relationship in the codebase, runs in milliseconds, and fails the moment someone adds a conflicting mapping. It also catches the other configuration-time warnings worth knowing about — a viewonly collection being mutated, a relationship with no back_populates where one was clearly intended, and cascade settings SQLAlchemy considers suspicious.
Make the suite strict. Add the filter to the project's pytest configuration so warnings are errors everywhere, not just in that one test:
# pyproject.toml / pytest.ini
[tool.pytest.ini_options]
filterwarnings = [
"error::sqlalchemy.exc.SAWarning",
# Silence a specific known-good case, with a comment explaining why:
# "ignore:relationship 'Order.products' will copy:sqlalchemy.exc.SAWarning",
]
Listing an exception explicitly, with a reason, is much better than a blanket filter: the next person sees which warning was accepted and why, and every other warning still fails the build.
The same strictness is what catches the relationship-adjacent problems this section deals with elsewhere: SAWarning: DELETE statement on table ... expected to delete N row(s) from a cascade fighting the database, and the cartesian-product warning from a join that should have been an EXISTS. None of them break anything on the day they appear, and all of them describe a model that does not mean what its author thought.
Frequently Asked Questions
What does "will copy column ... which conflicts with relationship(s)" mean?
Two relationships both maintain the same foreign key column at flush time. SQLAlchemy is warning that which one writes last is not defined by your code, so the two can disagree.
Is back_populates or backref preferred in SQLAlchemy 2.0?
back_populates. It declares both ends explicitly where they are defined, works with typed Mapped[] annotations, and makes this warning impossible for the common case.
When is overlaps the right fix?
Only when the columns are genuinely shared between two different relationships — typically composite keys containing a tenant or account column — and you have verified that they always write the same value. It asserts; it does not change behaviour.
Can I just ignore the warning?
If the mapping is a read-only view, make that explicit with viewonly=True instead. An ignored warning leaves two persistence rules competing for one column, which eventually writes a value nobody intended.
Related
- Modeling Relationships, Cascades and Association Objects — The parent guide: relationship shapes, loading and ownership.
- Using association objects for many-to-many with extra columns — The mapping that most often triggers this warning.
- Replacing backref with back_populates in SQLAlchemy 2.0 — Converting legacy implicit back-references.
- Typing relationships with Mappedlist in SQLAlchemy — Annotations that make both ends visible to type checkers.