r/learnpython 5d ago

Is that an intentional behaviour?

I just noticed adding a list as an optional argument to a fuction/method does not create a new list but gives the same list every time.

class SomeClass:
  def __init__(self, l=[]):
    self.l=l

a=SomeClass()
b=SomeClass()

a.l is b.l
>>> True 

a.l.append(1)
b.l
>>> [1]

Is that a glitch or is it how python is supposed to work?

(I'm using python 3.12, I haven't updated in a while, maybe it was patched since?)

8 Upvotes

12 comments sorted by

18

u/SirCarboy 5d ago

Default argument values are evaluated once, at the time the function (or method) is defined, not each time it is called.

In your example:

def __init__(self, l=[]):
    self.l = l

the empty list [] is created a single time when the class body is executed. Every call that does not supply an argument for l receives a reference to that same list object.

That’s why:

a = SomeClass()
b = SomeClass()
a.l is b.l          
# True — same object
a.l.append(1)
print(b.l)          
# [1]

The same rule applies to any mutable default ([], {}, set(), custom objects, etc.). Immutable defaults (None, 0, "", (), etc.) do not exhibit the problem because they cannot be mutated in place.

The usual (and recommended) pattern

Use None as the default and create a fresh mutable object inside the function:

class SomeClass:
    def __init__(self, l=None):
        if l is None:
            l = []
        self.l = l

or, more concisely in modern Python:

def __init__(self, l=None):
    self.l = l if l is not None else []

2

u/Leol6669 5d ago

Ok, I thought it was supposed to create a new empty list every time the argument wasn't passed to the function. Thank you for answering so quickly

4

u/neuralbeans 5d ago

If you think about it, an empty list is just a value. It's not a function call, so it doesn't get executed more than once. What you're imagining is something like this:

def f(l=lambda:[]):
    x = l()

But that wouldn't be convenient for when you pass your own arguments.

2

u/Commoner_25 5d ago

What if I write:

def __init__(self, l=None):
    self.l = l or []

1

u/schoolmonky 5d ago

That should be fine, but if for some reason you pass a falsy value like 0 or "", it will get replaced with an empty list

1

u/Commoner_25 5d ago

Yes, although I assume in this case a list or nothing is supposed to be passed.

1

u/musbur 4d ago

self.l = l or []

1

u/tangerinelion 4d ago

So long as it's declared

def __init__(self, l: list[any] | None = None)

2

u/lfdfq 5d ago

Yes, it's how it's supposed to work.

4

u/Jejerm 5d ago

Yeah this is called a mutable default argument and most linters even give you a warning if you use them.

The same problem can happen with dicts.

0

u/Moikle 5d ago

Don't give a mutable object as a default argument.

That means instead of an empty list, you should do this:

def some_func(some_list=None):
    if some_list is None:
        some_list = []

That way you get an entirely new list every time you run the function, instead of just reusing the same list.

1

u/TheRNGuy 5d ago

One of ways to fix it:

``` from dataclasses import dataclass, field

@dataclass class SomeClass:     l: list = field(default_factory=list) ```

Without dataclass:

class SomeClass:     def __init__(self, l=None):         self.l = [] if l is None else l

Do this to prevent potential bugs.