1

Consider the code below:

class Color:
    RED: "Color"
    GREEN: "Color"
    BLUE: "Color"
    WHITE: "Color"
    BLACK: "Color"

    def __init__(self, r: int, g: int, b: int) -> None:
        self.r = r
        self.g = g
        self.b = b


Color.RED = Color(255, 0, 0)
Color.GREEN = Color(0, 255, 0)
Color.BLUE = Color(0, 0, 255)
Color.WHITE = Color(255, 255, 255)
Color.BLACK = Color(0, 0, 0)

Here, I am creating a few color definitions, which can be accessed from the Color class, as well as creating custom color instances. However, it feels a little repetitive needing to declare then instantiate the values in two different places in my file. Optimally, I would do the following, but because it is self-referencing, I get a NameError.

class Color:
    RED = Color(255, 0, 0)
    GREEN = Color(0, 255, 0)
    BLUE = Color(0, 0, 255)
    WHITE = Color(255, 255, 255)
    BLACK = Color(0, 0, 0)

    def __init__(self, r: int, g: int, b: int) -> None:
        self.r = r
        self.g = g
        self.b = b

Is there a way for me to cleanly define my preset colors in one place whilst maintaining type safety and readability, or is the first example already as good as it's going to get?

13
  • Type hinting is laying out explicitly what the expected types are, and in the first place you define a slot, the second place you set the actual value, so it isn't repeated exactly, though that's what it looks like in Python. Commented Jan 4, 2023 at 4:35
  • For any other kind of value, I would be able to declare and define it in the same place. The only issue is that if it's the same type, it self-references and I get an error. I'm looking for a way I can declare and define it in the same place without getting an error. Commented Jan 4, 2023 at 4:38
  • Nope, entirely not possible because Color does not exist while you are defining it, so you will not be able to construct instances of itself while inside its class scope Commented Jan 4, 2023 at 4:40
  • Are you sure there isn't some kind of magic I can do with property or anything? Commented Jan 4, 2023 at 4:40
  • 2
    "It doesn't really address your issue" simply because you would be writing a lot more code, effectively building a "classproperty" method for each of the colors and to make the code formatting PEP-8 compliant you will need the decorator also need two lines for the method and return, plus an empty line, below, for each of these methods. Tell me how this is less work than what you had in the first place. Commented Jan 4, 2023 at 4:56

1 Answer 1

3

I think there are two approaches you could use for this. You could define a decorator for a property that attaches to the class:

class classproperty(property):
    def __get__(self, _, cls):
        return self.fget(cls)

class Color:
    @classproperty
    def RED(cls) -> "Color":
        return cls(255,0,0)
    @classproperty
    def GREEN(cls) -> "Color":
        return cls(0,255,0)
    @classproperty
    def BLUE(cls) -> "Color":
        return cls(0,0,255)
    @classproperty
    def WHITE(cls) -> "Color":
        return cls(255,255,255)
    @classproperty
    def BLACK(cls) -> "Color":
        return cls(0,0,0)

    def __init__(self, r: int, g: int, b: int) -> None:
        self.r = r
        self.g = g
        self.b = b

or you could use a metaclass that builds the properties in when the 'object' that represents the class type is being formed:

class __Color__metaclass__(type):
    @property
    def RED(cls) -> "Color":
        return cls(255,0,0)
    @property
    def GREEN(cls) -> "Color":
        return cls(0,255,0)
    @property
    def BLUE(cls) -> "Color":
        return cls(0,0,255)
    @property
    def WHITE(cls) -> "Color":
        return cls(255,255,255)
    @property
    def BLACK(cls) -> "Color":
        return cls(0,0,0)

class Color(metaclass=__Color__metaclass__):
    def __init__(self, r: int, g: int, b: int) -> None:
        self.r = r
        self.g = g
        self.b = b

I personally prefer the metaclass since for some reason my IDE can work out the autocompletions from that and yet can't on my decorated "class properties".

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for this solution! Unfortunately, Mypy and Pylance both reveal_type(Color.RED) incorrectly on both solutions: Revealed type is "Any" (mypy); Type of "c" is "classproperty" (Pylance). Similar issues are present for the second solution. I wonder if you could use property as a function rather than as a decorator to create a custom property type which does the construction under the hood (which would be even cleaner than the above).
Thinking now it has to be the metaclass. Once I added the annotations to the metaclass, reveal_type(Color.RED) says Color.
Which IDE are you using, out of curiosity. I have Pylance with VS Code, and use Mypy in CI, neither of which work correctly.
VS Code 1.74.2 ; Pylance v2023.1.10. What do you get when you hover Color.RED in your IDE, and what does reveal_type(Color.RED) give in mypy?
Ah - found something in how I was (incorrectly) testing the classproperty version, and that's not going to work for your use case. mypy can't work out what we are doing with the classproperty, so if we type hint then it thinks we are trying to call something that isn't callable, and if we don't then it decides the type is 'Any'. It seems to understand the metaclass though on my system. Weird we get different results.

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.