Everything being passed by object reference just means every case is equally unclear.

  answer = frobnicate(foo)
Will frobnicate destroy foo or not?

If you mean that it can modify it, you should say that. It can't destroy it as that term is generally understood.

No. It can’t. It can only destroy its own reference to foo, not the calling scope’s reference.

Right, but I don't care about the reference to foo (that's a low-level detail that should be confined to systems languages, not application languages) I was asking about the foo.

foo is a name. It's not at all clear what you mean by "the foo" ... the called function can modify the object referenced by the symbol foo unless it's immutable. If this is your complaint, then solve it with documentation ... I never write a function, in any language, that modifies anything--via parameter or in an outer scope--without documenting it as doing so.

> I don't care about the reference to foo (that's a low-level detail that should be confined to systems languages, not application languages)

This is not true at all. There's a big difference, for instance, between assigning a reference and assigning an object ... the former results in two names referring to the same object, whereas in the latter case they refer to copies. I had a recent case where a bug was introduced when converting Perl to Python because Perl arrays have value semantics whereas Python lists have reference semantics.

There seem to be disagreements here due entirely to erroneous or poor use of terminology, which is frustrating ... I won't participate further.

>> I sorely miss it in Python, JS and other languages. They keep me guessing whether a function will mutate the parent structure, or a local copy in those languages!

> Python at least is very clear about this ... everything, lists, class instances, dicts, tuples, strings, ints, floats ... are all passed by object reference. (Of course it's not relevant for tuples and scalars, which are immutable.)

Then let me just FTFY based on what you've said later:

Python will not be very clear about this ... everything, lists, class instances, dicts, tuples, strings, ints, floats, they all require the programmer to read and write documentation every time.

As they should, of course. And this is no different from the annotations that started this conversation, other than that one is enforced by the compiler ... but you never mentioned that.

I won't comment further.

Right, but that reference is all the function has. It can’t destroy another scope’s reference to the foo, and the Python GC won’t destroy the foo as long as a reference to it exists.

The function could mutate foo to be empty, if foo is mutable, but it can’t make it not exist.

>> I sorely miss it in Python, JS and other languages. They keep me guessing whether a function will mutate the parent structure, or a local copy in those languages!

No mention of references!

I don't care about references to foo. I don't care about facades to foo. I don't care about decorators of foo. I don't care about memory segments of foo.

"Did someone eat my lunch in the work fridge?"

"Well at least you wrote your name in permanent marker on your lunchbox, so that should help narrow it down"

Then I don’t know what you mean. If you have:

  foo = open(‘bar.txt’)
  answer = frobnicate(foo)
  print(foo)
then frobnicate may call foo.close(), or it may read foo’s contents so that you’d have to seek back to the beginning before you could read them a second time. There’s literally nothing you can do in frobnicate that can make it such that the 3rd raises a NameError because foo no longer exists.

In other words,

>> I sorely miss it in Python, JS and other languages. They keep me guessing whether a function will mutate the parent structure, or a local copy in those languages!

  #!/usr/bin/env python3
  import inspect
  
  def frobnicate(unfrobbed: any) -> None:
      frame = inspect.currentframe().f_back
      for name in [name for name, value in frame.f_locals.items() if value is unfrobbed]:
          del frame.f_locals[name]
      for name in [name for name, value in frame.f_globals.items() if value is unfrobbed]:
          del frame.f_globals[name]
  
  foo = open("bar.txt")
  answer = frobnicate(foo)
  print(foo)

  
  Traceback (most recent call last):
    File "hackers.py", line 20, in <module>
      print(foo)
            ^^^
  NameError: name 'foo' is not defined
Be careful with the absolutes now :)

Not that this is is reasonable code to encounter in the wild, but you certainly can do this. You could even make it work properly when called from inside functions that use `fastlocals` if you're willing to commit even more reprehensible crimes and rewrite the `f_code` object.

Anyway, it's not really accurate to say that Python passes by reference, because Python has no concept of references. It passes by assignment. This is perfectly analogous to passing by pointer in C, which also can be used to implement reference semantics, but it ISN'T reference semantics. The difference comes in assignment, like in the following C++ program:

  #include <print>
  
  struct Object
  {
      char member{'a'};
  };
  
  void assign_pointer(Object *ptr)
  {
      Object replacement{'b'};
      ptr = &replacement;
  }
  
  void assign_reference(Object &ref)
  {
      Object replacement{'b'};
      ref = replacement;
  }
  
  int main()
  {
      Object obj{};
      std::println("Original value: {}", obj.member);
      assign_pointer(&obj);
      std::println("After assign_pointer: {}", obj.member);
      assign_reference(obj);
      std::println("After assign_reference: {}", obj.member);
      return 0;
  }

  $ ./a.out
  Original value: a
  After assign_pointer: a
  After assign_reference: b

Just like in Python, you can modify the underlying object in the pointer example by dereferencing it, but if you just assign the name to a new value, that doesn't rebind the original object. So it isn't an actual reference, it's a name that's assigned to the same thing.

ANYWAY, irrelevant nitpicking aside, I do think Python has a problem here, but its reference semantics are kind of a red herring. Python's concept of `const` is simply far too coarse. Constness is applied and enforced at the class level, not the object, function, or function call level. This, in combination with the pass-by-assignment semantics does indeed mean that functions can freely modify their arguments the vast majority of the time, with no real contract for making sure they don't do that.

In practice, I think this is handled well enough at a culture level that it's not the worst thing in the world, and I understand Python's general reluctance to introduce new technical concepts when it doesn't strictly have to, but it's definitely a bit of a footgun. Can be hard to wrap your head around too.

They seem to be using "destroy" in some colloquial sense, actually meaning "modify".

I'm truly not sure, but you're probably right.