1

Suppose I have an instance attribute that I don't initialize in __init__, but in normal use it should be initialized before any other methods use the value. I want to structure everything so that it all type-checks, and I'd like your opinion on which of these structures is best. My first pass I would write something like this.

class Widget:
    def __init__(self):
        self.handle: Optional[int] = None

    # must be called before calling any other methods
    def on_start(self, handle): 
        self.handle = handle

    def use_handle(self):
        pass # do stuff with self.handle as an int

Let's assume use_handle and all other methods really only work if on_start was called first. The issue with this design is that use_handle assumes self.handle is an int, and doesn't check for None, so it will fail the type-check. So I could add a None check to every method, but with many attributes and methods this becomes overwhelming to have every method do a None check for every attribute that couldn't be initialized until on_start was called.

My next pass would be to define a getter:

class Widget:
    def __init__(self):
        self._handle: Optional[int] = None

    # must be called before calling any other methods
    def on_start(self, handle): 
        self._handle = handle

    @property
    handle(self):
        if self._handle is None:
            raise RuntimeError('handle was accessed before initialization')
        return self._handle

    def use_handle(self):
        pass # do stuff with self.handle as an int

Now it type-checks but I'm throwing an error if accessed before initialization. Sounds kind of like the default behavior if I just didn't define a default value in __init__ in the first place:

class Widget:
    def __init__(self):
        self.handle: int # don't initialize it, just type hint it

    # must be called before calling any other methods
    def on_start(self, handle): 
        self.handle = handle

    def use_handle(self):
        pass # do stuff with self.handle as an int

So which of these (or other designs) do you prefer?

3
  • 3
    I think I prefer the explicit error but I would try to avoid this kind of thing in the first place. Can you elaborate why you need to create the object before you know how to fully initialize it?
    – JimmyJames
    Commented Oct 14, 2021 at 17:58
  • I definitely try to avoid this if I can, maybe it just requires more design thought, but currently I have an application that holds a reference to it's window handle. The applications starts the window so the window handle is not available until after the window is created. I'm toying around with how to start the window before the application but stumbling over OpenGL contexts and such. Even if I (hopefully) resolve this window issue, I feel like this is sometimes unavoidable in chicken-egg situations where there are meaningful cycles in the object graph.
    – nullUser
    Commented Oct 14, 2021 at 18:38
  • 1
    Maybe it's a good separate question and I don't have any experience with OpenGL and Python. But there may be ways out of this. One idea that comes to mind is to 'inject' the object into the context where this handle is used after the handle is known i.e. in the on_start method. You can either use this existing design or create another object from it. Either way, if the object is not accessible to the code that would try to access the handle until after it's known set, there's no way for it to attempt to use it too early.
    – JimmyJames
    Commented Oct 14, 2021 at 18:58

1 Answer 1

4

A design in which the object changes it shape / available fields depending on state is generally not a good design.

Your solutions in which you model the field as an Optional[int] with a None default value make this statefulness explicit. I think that makes the best of this design. You already have this state dependency, so modelling it on the type level makes sure you check for the correct state wherever necessary.

In your second solution, you partially hide this statefulness by directing access to the stateful value through a property. This is good, because it enforces appropriate checks before using the value. The downside is that other parts of the code no longer see this statefulness, and might get an unexpected exception instead.

Your third solution is very “Pythonic” in that it is closest to your original design, but it essentially involves a lie: you are making a statement about the type of a field that may or may not exist.

Personally, I would use the following decisions to select a design:

  • If this class is used purely internally and it is easy to see that all accesses to this value will be valid, then the third solution seems to be the most simple (field is given a type but not initialized).

  • If the class is used externally or if the use of this class isn't obviously correct, then one of the first two solutions seems more appropriate.

  • If any access to the value must be correct by definition, a property that throws an exception sounds reasonable. In particular, this sounds appropriate if there is a constraint “on_start() must be called before before use_handle()”. Such temporal coupling is not ideal, but sometimes unavoidable.

  • If other code needs to inspect in which state the object is, then the first solution is most appropriate. For example, consider an at-most-once initialization pattern like this:

    # supply a handle if necessary
    if widget.handle is None:
        widget.on_start(default_handle)
    

All of that is under the premise that this statefulness is actually needed. For complex object graphs, that can be the case legitimately. But in many scenarios, such statefulness is not actually necessary. Life tends to be simpler if an object can only exist in a fully-initialized form. In particular, tree-shaped object graphs (every object has a single owner) do allow complete construction if all necessary data is provided to the root object constructor which then forwards data to sub-objects.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.