How to Put Keyword Arguments in your Python Class Definitions

marvintensuan

Marvin

Posted on January 15, 2022

How to Put Keyword Arguments in your Python Class Definitions

I have recently looked at using a library called SQLModel. SQLModel is an ORM being developed by the same guy behind FastAPI, Sebastián Ramírez. This library combines pydantic and SQLAlchemy to give users a new and hopefully better way to define their Models.

This is an example model that inherits from SQLModel.

from sqlmodel import SQLModel, Field

class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True) 
    name: str 
    secret_name: str 
    age: int | None
Enter fullscreen mode Exit fullscreen mode

Not gonna lie, defining database models the same way you would a dataclass sounds cool. Now, take a closer look at this syntax table=True beside the class definition. This technique is new to me (and off the record guys, I had to read through some SQLALchemy codes for a few hours simply because I explicitly forgot to put it in my model).

When you think about it, ORM models are definitely one of the places where it makes sense to put keyword arguments in the class definition itself. But how would one do it? How would someone prompt users to put a keyword argument in their class definitions?

Using a Metaclass

As the saying goes, if you have to ask why you would need a metaclass, then you probably don't need it. But now that we have a use case, let's have a discussion.

Refer to the code below:

class StudentMeta(type):
    def __new__(cls, *args, **kwargs):
        print(f"{args = }")
        print(f"{kwargs = }")
        return super().__new__(cls, *args)

class Student(metaclass=StudentMeta, table=True):
    ...

troy = Student()
Enter fullscreen mode Exit fullscreen mode

StudentMeta is our metaclass, Student is our class, and troy is a Student object.

Below is the output of the code:

args = ('Student', (), {'__module__': '__main__', '__qualname__': 'Student'})
kwargs = {'table': True}
Enter fullscreen mode Exit fullscreen mode

Note that args is a tuple with 3 elements. Traditionally, these are called name, bases, and dict (not to be confused with the dictionary built-in).

You may go further and run the following:

print(f"{troy.__class__.__name__ = }")
print(f"{troy.__class__.__bases__ = }")
print(f"{troy.__class__.__dict__ = }")

# Output should be:
# troy.__class__.__name__ = 'Student'
# troy.__class__.__bases__ = (<class 'object'>,)
# troy.__class__.__dict__ = mappingproxy({'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Student' objects>, '__weakref__': <attribute '__weakref__' of 'Student' objects>,'__doc__': None})
Enter fullscreen mode Exit fullscreen mode

These args are the required arguments for when constructing a new type.

What's happening here is that similar to how 42 is an object of type int, and int is a class of type...

  • troy is an object of type Student;
  • Student is a class of type StudentMeta;
  • StudentMeta is a class of type.

StudentMeta is undergoing a normal type creation, except that we have it modified to print all args and kwargs used in the instantiation. Take note that the return value of __new__ is return super().__new__(cls, *args). This is because type(...) require only the name, bases, and dict to create a new type.

That leaves us with kwargs. Do note that print(f"{kwargs = }") was triggered when Student was defined.

Using __init__subclass__

__init__subclass__ use subclassing to modify the behavior of a Parent's subclass.

class Base:
    def __init_subclass__(cls, *args, **kwargs) -> None:
        print(f"{args = }")
        print(f"{kwargs = }")
        super().__init_subclass__()

class Student(Base, table=True):
    ...

abed = Student()
Enter fullscreen mode Exit fullscreen mode

In the example above,

  • abed is an object of type Student;
  • Student is a class of type;
  • Base is a class of type.

And the output is below:

args = ()
kwargs = {'table': True}
Enter fullscreen mode Exit fullscreen mode

There are no args because we did not have to create a new type; __init__subclass__ only modifies the creation of a new child class (which is what we did in the earlier example). Note that __init__subclass__ does not have a return statement.

Not only that but ...

print(f"{abed.__class__.__name__ = }")
print(f"{abed.__class__.__bases__ = }")
print(f"{abed.__class__.__dict__ = }")

# Output should be:
# abed.__class__.__name__ = 'Student'
# abed.__class__.__bases__ = (<class '__main__.Base'>,)
# abed.__class__.__dict__ = mappingproxy({'__module__': '__main__', '__doc__': None})
Enter fullscreen mode Exit fullscreen mode

Putting it all together

In this article, we covered two ways to use keyword arguments in your class definitions. Metaclasses offer a way to modify the type creation of classes. A simpler way would be to use __init__subclass__ which modifies only the behavior of the child class' creation.

Regardless of the method, these keyword arguments can only be used during the creation of a class.

💖 💪 🙅 🚩
marvintensuan
Marvin

Posted on January 15, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related