Functions in class bodies that aren't methods

A recent thread about the proposed extension of set methods raised a question I often wondered about, and which I find interesting because it’s not really settled : is the first parameter of a function defined in a class body (or found in a class’s scope) already special ?

(In context this was more specifically about using an *args parameter to catch the first argument along with many others, but I think the point applies just the same.)

On the one hand, several languages, including C++, JavaScript and Java if memory serves correctly, allow cls.function to be used as a function taking an instance of cls instead of being a method accessed on such an instance. Python functions-in-classes support that as well.
In addition to that, Python-coded functions (in CPython at least) do support passing a value of any type as the first parameter, with no check being made at any point in the process:

>>> class S(str):
...     def ad(self, other):
...             return self+other
...     def __init__(self):
...             raise NotImplementedError
...
>>> S.ad(5, 3)
8

Taking advantage of that feature can be useful, in order (for example, and in addition to the example given in the thread) to make a function either extract data on an instance of a certain type, or take the same data directly.
Take the inspect.Signature class for example : it mainly represents a sequence of Parameter objects, with some additional constraints - for one, you can’t order them in any way you want. Now, say I want to create a function on sequences of Parameter objects with any order.
I could make two methods on the Signature class (or my own subclass), one static taking a sequence and one method extracting the list of Parameters from the instance it’s called on and passing it to the static one. But they would need to have different names.
I could make a function outside of the class to take either a Signature or a sequence, but that function really belongs in/with the Signature class so it’s a shame to have to separate them.
I could almost use functools.single_dispatch, to group under the same name the method and static method described above , but I would need to pass the class to the decorator, and that can’t be done from inside the class’s own body.

On the other hand, type checkers completely ignore this “feature” by assuming the first parameter is always an instance of the class, even when you write self=5 for example. Ones I use at least behave as follows:

class A:
    def a(self=5):
        # self is typed as "A", or as "Self@A"
        if isinstance(self, A):
            ...
        else:
            # self is typed as "Nothing" here even though this code is perfectly reachable
            ...

This is also impossible on methods of C-builtin types - the ones I tried it on at least - which could be interpreted as a sign that this is only an implementation detail and not a feature. However, built-in functions are already known as having limitation compared to their pure-Python counterparts : the ability to set arbitrary attributes on them for example, or in some cases to pass parameters by keyword.

I believe this question would gain from being settled. However unprecedented in the stdlib and in native types, is it a feature to be able to pass values of any type to the first parameter of functions defined (in pure Python) in class bodies or found in classes’ scopes ?

The first arg to a function defined in a class will always be passed the instance as its first arg. By convention that first are is named self.

You can use staticmethod decorator to make a function that does not take an instance. See Built-in Functions — Python 3.12.1 documentation

class Klass:
    @staticmethod
    def not_a_method(any_arg):
        print(arny_arg)

True you cannot set args on a C-function, but you can define functions in C that take keyword args and positional args. Sometimes the programmer for design reasons may not implement all features.

I am aware. Please read my post again.

1 Like

It used to be that if you looked up an ordinary function from a class
you got an “unbound method” object that raised an exception if the
first argument wasn’t an instance of the class. If you didn’t want that
you had to use the @staticmethod decorator – but then you couldn’t use
the function as an instance method.

At some point (I don’t remember exactly when) unbound methods were
abolished, making it possible to use the same function as either a
static method or an instance method.

I would take this as a strong suggestion that this ability is an
intended feature.

I don’t know what type checkers should do about it, though. I think
it’s correct for a type checker to assume this behaviour is not
intended the vast majority of the time. Maybe there should be a
way of telling a type checker that it’s okay in particular cases?
Such as declaring “self” to be a union of the class type and
something else.

I think I agree. The current type checker/linter behavior should be the default, but there should be a (preferably simple) way to disable it, or to mark it as intentional in the case of linters.

Yes. The language model doesn’t explicitly exclude it because functions defined in a class scope are just functions bound to the class object. The fact instance methods do something special is due to separate details in the language spec.

2 Likes

This behavior also has type checker support, you just have to annotate self: mypy Playground

self is treated differently from other arguments when not specified by type checkers, rather than defaulting to Any when unspecified it defaults to the type you call the method on. This is a convenience feature, not a restriction on self.

1 Like

I was not clear what point you are trying to make and replied to what it seemed where the gaps in explaination of class method types.

2 Likes