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:

Two owners, one column Four steps. Two relationships are declared over the same foreign key without back_populates, so the mapper treats them as unrelated. Code appends to one of them, which schedules a write of order_lines.order_id. Code also sets the other, which schedules its own write of the same column. At flush the two writes are applied in an order the unit of work chooses, and the value that survives depends on that order rather than on the code. two relationships, same foreign key no back_populates the mapper sees two owners order.lines.append(line) schedules order_id = order.id via relationship A line.order = other_order schedules order_id = other.id via relationship B flush one write wins, silently The warning is emitted at mapper configuration, long before any flush makes the conflict visible.
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.

Three causes, three fixes Three tiles. Two halves of the same relationship declared separately: link them with back_populates so the mapper treats them as one bidirectional relationship. A relationship kept only for reading, such as a secondary alongside a mapped association object: mark it viewonly=True. Genuinely overlapping foreign keys, such as a composite key shared by two relationships: declare the overlap with the overlaps parameter after checking only one of them writes. two halves of one relationship no back_populates → back_populates="..." a read-only second view secondary + association object → viewonly=True genuinely shared columns composite or partial keys → overlaps="..." Reach for overlaps last: it silences the warning without changing behaviour, so the write path must already be correct.

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 symptomRoot CauseProduction 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 secondaryA convenience relationship alongside an association object.viewonly=True on the secondary relationship.
The warning names relationships on different classes sharing one columnOverlapping 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 refreshTwo unlinked relationships, so nothing synchronises in memory.back_populates.
ArgumentError: relationship X will copy column ... but there is no foreign keyforeign_keys or primaryjoin not set where the join is ambiguous.Name them explicitly.
The warning disappeared but a column is still written unexpectedlyoverlaps was used to silence a real conflict.Make the non-writing relationship viewonly=True instead.
Warning appears only in productionWarnings filtered in the test configuration.Run tests with -W error::sqlalchemy.exc.SAWarning.
back_populates makes them one relationship Left: Order.lines and OrderLine.order are declared independently, so the mapper maintains order_lines.order_id twice and warns; appending to one collection does not update the other attribute in memory. Right: each names the other with back_populates, so the pair is one bidirectional relationship, synchronised in memory and written once. declared separately lines = relationship("OrderLine") order = relationship("Order") will copy column ... conflicts in-memory sides drift apart linked with back_populates lines = relationship(back_populates="order") order = relationship(back_populates="lines") one relationship, two views appending sets the other side too back_populates replaced backref as the explicit form: both sides are visible where they are declared.

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.

Reading the warning Four steps. The message names the relationship doing the copying and the column pair it writes. It then names the conflicting relationships and the columns they write. Comparing the two tells you whether they are the same link seen from both ends, a read-only duplicate, or two different links that happen to share a column. Only then choose between back_populates, viewonly and overlaps. 1 · read the first clause: relationship 'X' will copy column a.id to column c.a_id that is the relationship SQLAlchemy is describing, and the column it maintains 2 · read the conflict clause: conflicts with relationship(s) 'Y' Y writes the same column; the message prints which columns each one copies 3 · decide what X and Y are to each other two ends of one link, a read-only duplicate, or two links sharing a column 4 · apply back_populates, viewonly=True, or overlaps="..." in that order of preference — the last one only asserts, it does not fix
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.

overlaps asserts; viewonly enforces Left: overlaps names the other relationships and tells SQLAlchemy the overlap is intended; both relationships remain writable, so a wrong assertion leaves the original ambiguity in place. Right: viewonly makes one relationship read-only, so the mapper knows there is exactly one writer and nothing can conflict at flush time. overlaps="items,product" the warning disappears both sides still write an incorrect assertion hides a bug for genuinely shared columns viewonly=True the warning disappears only one writer remains writes through it are ignored for any read-only view If a relationship exists only so that code can read through it, viewonly is the honest declaration.

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.