How to Put Keyword Arguments in your Python Class Definitions
Marvin
Posted on January 15, 2022
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
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()
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}
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})
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 typeStudent
; -
Student
is a class of typeStudentMeta
; -
StudentMeta
is a class oftype
.
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()
In the example above,
-
abed
is an object of typeStudent
; -
Student
is a class oftype
; -
Base
is a class oftype
.
And the output is below:
args = ()
kwargs = {'table': True}
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})
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.
Posted on January 15, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.