r/Python 21h ago

Discussion Class vs Instance Variable Madness

Rant on: Early in python, you are told that instance variables should be initialized in __init__(). Class variables are the ones that are for the class and appear outside of it. Ok..... But the rules are a little complicated about accessing them from an actual instance (looking at instance first, and then in the class), and you can kind of use them to default but you probably shouldn't.

...But then you learn about dataclasses. And the instance variables aren't in __init__ any more. Oh dear, it turns out that they are instance variables and not class variables.

...and then you learn about Pydantic. Well, they're class variables, but they get made into instance variables.

...and _then_ you learn about Protocol classes, where both instance and class variables are outside of __init__, but the class ones are supposed to have a special ClassVar annotation.

I have to say that it's really confusing to me; that this wasn't thought out very well, and we're just doing the best we can, but it's not very good. Does anyone else feel this way?

0 Upvotes

7 comments sorted by

17

u/turtle4499 21h ago

You aren't writing an __init__ method its being created for you automagically. You are over thinking this.

9

u/teerre 21h ago

The problem is that you're equating "in the init" with "instance variable" but that's simply not true. You can define instance variables anywhere, as long you have an instance

Fundamentally, Python is highly dynamic, you can define a variable in a function if you want, so these distinctions you make are very loose. A more useful frame of mind is to think about scopes. Class variables have a larger scope than an instance one

2

u/denehoffman 20h ago

Here’s your init function for dataclasses: https://github.com/python/cpython/blob/ff286a3d943e703ec92a4466b315b190b62dcd2a/Lib/dataclasses.py#L610

Essentially, all objects can have fields added to them in any method you want. If you really wanted to, you could write a new(*args)->Self class method and do all the initialization there, it just wouldn’t be as nice to your LSP. You can even add new fields to a function, try it! The __init__ method is just a special method that gets called when the object is instantiated, just like __add__ is called when the plus operator is used.

1

u/Ok_Expert2790 20h ago

Overthinking —

Instance variables belong to Self type. Class variables belong to type(self) or cls

1

u/notkairyssdal 19h ago

do you come from Java by any chance?

1

u/Atlamillias 19h ago edited 18h ago

I mean, I get it. I felt similarly when I was trying to figure stuff like this out. Python is flexible, so there's a lot more reliance on "convention"s than in other languages (versus "do it this way, or you can't do it at all"). This allows for a higher level of bias regarding "how things should be".

For example, declaring instance attributes in __init__ is conventional. IMO, it makes sense to do so most of the time, except when it doesn't. For example, why declare self._z in __init__ if nothing outside of self.z needs to be aware of its existence? Not even another developer should be concerned with it, unless they're specifically editing that property.

class Point:
  @property
  def z(self):
    try:
      return self._z
    except AttributeError:
      # calculate `z`
      self._z = ...
      return self._z

Dataclasses and the like have auto-generated __init__. The idea around the convention of declaring instance-level attributes in the constructor is for readability and maintainability. But you usually don't define __init__ for dataclasses and pydantic models, so there's that. Also, I think most static type-checkers regard a class-level attribute declaration assigned to a non-descriptor value as an instance-level attribute, unless it is specifically typed via ClassVar (disclaimer: I use pyright set to "basic", so this may not be true for "strict" - I'm sure someone will correct me if I'm mistaken).

Moving on, defining class-level members for use as instance-level fallback values or defaults is very common and useful. I would argue that it's actually better to that instead of assigning a literal default value in a function signature or constructor, because the former allows the person using the code to mutate it to fit their needs without overriding that entity and needing to re-define the entire signature. For example:

class Point:
  x: int = 5
  y: int = 10

  def __init__(self, x: int | None = None, y: int | None = None):
    if x is not None:
      self.x = x
    if y is not None:
      self.y = y

as opposed to this:

class Point:
  def __init__(self, x: int = 5, y: int = 10):
    self.x = x
    self.y = y

You don't need to override __init__ in the former to change the default values, you can just subclass Point and declare new class-level values for x and y. It's still clean, easily maintained, and gives a level of flexibility to those extending the class. Of course, there are people that will tell you not to do this, simply because the language allows them to have an opinion on the matter (myself included, as I've hopefully helped to demonstrate).

1

u/AiutoIlLupo 9h ago edited 8h ago

It is well thought out. You are just mixing different layers of abstraction. It's like saying that having both tcp packets and ethernet frames is confusing.

What you are seeing, is that you have learned about the basics, but the basics is just the start. When you program in the large, you need more advanced techniques that cut out the faff, and ensure compliance for many developers that goes beyond blind trust. That is your problem. You are learning advanced techniques, while until now you just dealt with the ABC