Ask
189
@csv_apologist ·

My function keeps returning results from the previous call and I cannot see how

I wrote something like def add_row(row, rows=[]) which appends the row and returns rows. Called once it is correct, called a second time it comes back with the first call's row still in it, and I am not storing anything between calls anywhere that I can see. This is a small script that reads a CSV and builds a list per file, so the leak between calls is producing nonsense output. What is actually happening here?

7 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @listcomp_leo · 2mo ago · 3 replies

    The default value is created once, when the def line runs, not each time you call the function. So there is exactly one list living on the function object, and every call that does not pass its own list appends to that same one. The fix is the None sentinel: take rows=None as the default, and on the first line of the body do rows = [] if rows is None else rows. Same trap applies to dicts, sets and anything else mutable you put in a default.

    214
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @listcomp_leo · 2mo ago

      Almost everyone assumes that. You can even see it — print add_row.defaults after a couple of calls and you will watch the list grow.

      74
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @csv_apologist · 2mo ago

      Created once at def time is the piece I did not have. I assumed the default was re-evaluated on every call.

      67
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @pinned_versions · 2mo ago · 2 replies

    Worth knowing that a linter catches this before you ever run it. Ruff and flake8 with the bugbear rules both flag a mutable default argument as a specific warning, and it takes about a minute to add to a project. The category of bug that a linter finds is small but it is entirely free, and this one is the flagship member of that category.

    158
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @first_repo_finn · 2mo ago

      Adding a linter as a beginner is also how you find out about half a dozen habits nobody told you were habits.

      56
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @tabula_rasa_dev · 2mo ago

    I lost most of an evening to this exact shape of bug in a script that appended parsed records, and what made it so slow to find was that the first run was always right. I kept restarting the interpreter to test, which reset the default list, so the bug vanished every time I looked at it. If you are ever debugging something that only misbehaves on repeat calls, suspect state that outlives the call.

    152
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @kerf_wander · 2mo ago

    Slightly different framing from the top answer, which is correct about the mechanism but stops one step short. The real lesson is aliasing: in Python a name is a label attached to an object, and assignment binds another label rather than making a copy. Once that clicks, mutable defaults, the shared list inside a class attribute, and the classic list-of-lists made with multiplication all become the same bug rather than three separate gotchas to memorise.

    141
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report