> Don't get me started about teaching beginners which datatypes are pass by reference and which are pass by value.
If they are beginners to programming, you wouldn't teach them those terms in the context of Python, because neither of those terms map to Python argument passing; Python has one form of argument passing, and it doesn't map closely to the intuition that experienced programmers in languages that have pass by reference and pass by value have about those things. Likewise, the only thing you'd teach someone new to Python that is experienced in languages where those terms are useful is that that distinction is irrelevant in Python, which is pass by assignment (sometimes also called pass by object reference, but pass by assignment is a much more useful description IMO, because argument passing works exactly like assignment to a new variable name.)
> try explaining to an elementary school student why
def foo(a):
a = a + 1
> doesn't change the caller's variable but
def bar(a):
a.append(1)
> does.
But, that's easy, if you've first taught them how variables, assignment, and mutation work in Python without getting function calls in the way, because it is exactly the same as this
a = 1
b = a
b = a + 1
print(f"{a=}, {b=}")
vs.
a = [1]
b = a
b.append[1]
print(f"{a=}, {b=}")
Argument passing is just assignment to a new variable that exists in the scope of the function. Methods which mutate an object affect the object no matter what variable you access it from, assignment operations affect only the variable they assign to. That's exactly the same behavior in one scope as it is between the function and caller scopes.
And this distinction has nothing to do with data types, but only with the operations performed (the only connection to data types is that immutable types have no mutation operations in the first place.) You can tell its not about data types because you can use the same types as the second excerpt, and the operations of the first, and get the same results as the first (which shares operations) and not the second (which shares datatypes):
a = [1]
b = a
b = b + [1]
print(f"{a=}, {b=}")
If you understand how assignment and mutation works in one scope, you understand how argument passing works. Trying to teach a distinction that exists between how different
operations affect variables that initially reference the same object as a distinction about how datatypes are passed as arguments
is confusing, because you as a teacher are presenting the source of the difference in behavior as originating in a
completely different place than where it actually comes from. That's not a problem with Python being complex, it is a problem with you taking a very simple thing and making it complex by ascribing it to a source that is completely irrelevant to what is actually going on.