Problem solving · should-tier · asked at Amazon, Apple, Google, Meta, Microsoft, Netflix

Mutable Default Argument

easydebugging

Problem

A reporting service builds one Report per region and appends a summary row to each. The class looks harmless:

class Report:
    def __init__(self, title: str, rows: list[dict] = []):
        self.title = title
        self.rows = rows

    def add_row(self, **fields) -> None:
        self.rows.append(fields)


def build_daily_reports(day_stats: dict[str, dict]) -> list[Report]:
    reports = []
    for region, stats in day_stats.items():
        r = Report(f"Daily - {region}")
        r.add_row(**stats)
        reports.append(r)
    return reports

Symptom: the eu report contains the us row as well as its own, and after the service has been up for a day every report contains hundreds of rows from other regions and earlier days. Restarting the worker process "fixes" it for a while. Explain what is happening and fix it.

Examples

  • build_daily_reports({"us": {"orders": 10}, "eu": {"orders": 4}}) → expected us rows [{"orders": 10}], eu rows [{"orders": 4}]. Actual: both reports have rows [{"orders": 10}, {"orders": 4}], and reports[0].rows is reports[1].rows is True.
  • Calling build_daily_reports a second time in the same process → each new report starts with all rows from the previous call already present.
  • Report("x", rows=[{"a": 1}]) → correct, because an explicit list was passed. Only the default is broken, which is why the one test that passed rows explicitly was green.

Hints

  1. When is the expression [] in the signature evaluated: every call, or once?
  2. Print id(r.rows) for two different reports created without passing rows.
  3. Look at Report.__init__.__defaults__ after the first call.

Last reviewed 8 September 2026