Basics Intermediate Advanced
aialgorithmsapibest-practicescareercommunitydatabasesdata-sciencedata-structuresdata-vizdevopsdjangodockereditorsflaskfront-endgamedevguimachine-learningnewsnumpyprojectspythonstdlibtestingtoolsweb-devweb-scraping

Data Classes in Python (Guide)
Table of Contents
Recommended Course
A Python dataclass lets you define classes for storing data with less boilerplate. Use@dataclass to generate.__init__(),.__repr__(), and.__eq__() automatically. Dataclasses allow you to create classes quickly, but you can also add defaults, custom methods, ordering, immutability, inheritance, and even slots.
By the end of this tutorial, you’ll understand that:
- Mutable defaults use
field(default_factory=...), while simple defaults usefield(default=...)or an inline value. - Type hints are required for fields, but they’renot enforced at runtime. Use
typing.Anywhen needed. - Ordering comes from
@dataclass(order=True)and can be customized by computing a.sort_indexin.__post_init__(). - Immutability is enabled with
frozen=True, yet nestedmutable fields can still change if their types allow it. - Inheritance rules require that anynon-default field in a subclass cannot follow defaulted base-class fields.
When working through this tutorial, you’ll also get to compare dataclasses with Python’snamedtuple andattrs and identify when each option fits best.
Free Download:Get a sample chapter from Python Tricks: The Book that shows you Python’s best practices with simple examples you can apply instantly to write more beautiful + Pythonic code.
Take the Quiz: Test your knowledge with our interactive “Data Classes in Python” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Data Classes in PythonIn this quiz, you'll test your understanding of Python data classes.Data classes, a feature introduced in Python 3.7, are a type of class mainly used for storing data.They come with basic functionality already implemented, such as instance initialization, printing, and comparison.
Python’s Dataclass in a Nutshell
SincePython 3.7, Python ships with the@dataclassdecorator that allows you to create data classes. A data class is a class typically containing mainly data, although there aren’t really any restrictions. You create it using the@dataclass decorator, as follows:
fromdataclassesimportdataclass@dataclassclassDataClassCard:rank:strsuit:strA data class comes with basic functionality already implemented. For instance, you can instantiate, print, and compare data class instances straight out of the box:
>>>queen_of_hearts=DataClassCard('Q','Hearts')>>>queen_of_hearts.rank'Q'>>>queen_of_heartsDataClassCard(rank='Q', suit='Hearts')>>>queen_of_hearts==DataClassCard('Q','Hearts')TrueCompare that to a regular class. A minimal regular class would look something like this:
classRegularCard:def__init__(self,rank,suit):self.rank=rankself.suit=suitWhile this is not much more code to write, you can already see signs of the boilerplate pain:rank andsuit are both repeated three times simply to initialize an object. Furthermore, if you try to use this plain class, you’ll notice that the representation of the objects is not very descriptive, and for some reason a queen of hearts is not the same as a queen of hearts:
>>>queen_of_hearts=RegularCard('Q','Hearts')>>>queen_of_hearts.rank'Q'>>>queen_of_hearts<__main__.RegularCard object at 0x7fb6eee35d30>>>>queen_of_hearts==RegularCard('Q','Hearts')FalseSeems like data classes are helping us out behind the scenes. By default, data classes implement a.__repr__() method to provide a nice string representation and an.__eq__() method that can do basic object comparisons. For theRegularCard class to imitate the data class above, you need to add these methods as well:
classRegularCarddef__init__(self,rank,suit):self.rank=rankself.suit=suitdef__repr__(self):return(f'{self.__class__.__name__}'f'(rank={self.rank!r}, suit={self.suit!r})')def__eq__(self,other):ifother.__class__isnotself.__class__:returnNotImplementedreturn(self.rank,self.suit)==(other.rank,other.suit)In this tutorial, you’ll learn exactly which conveniences data classes provide. In addition to nice representations and comparisons, you’ll see:
- How to add default values to data class fields
- How data classes allow for ordering of objects
- How to represent immutable data
- How data classes handle inheritance
You’ll soon dive deeper into those features of data classes. However, you might be thinking that you have already seen something like this before. And indeed, there are similar constructs in Python that you can revisit in the next section.
Alternatives to Data Classes
For simple data structures, you have probably already usedatuple or adict. You could represent the queen of hearts card in either of the following ways:
>>>queen_of_hearts_tuple=('Q','Hearts')>>>queen_of_hearts_dict={'rank':'Q','suit':'Hearts'}It works. However, it puts a lot of responsibility on you as a programmer:
- You need to remember that the
queen_of_hearts_...variable represents a card. - For the
tupleversion, you need to remember the order of the attributes. Writing('Spades', 'A')will mess up your program but probably not give you an easily understandable error message. - If you use the
dictversion, you must make sure the names of the attributes are consistent. For instance{'value': 'A', 'suit': 'Spades'}will not work as expected.
Furthermore, using these structures is not ideal:
>>>queen_of_hearts_tuple[0]# No named access'Q'>>>queen_of_hearts_dict['suit']# Would be nicer with .suit'Hearts'A better alternative is thenamedtuple. It has long been used to create readable small data structures. We can in fact recreate the data class example above using anamedtuple like this:
fromcollectionsimportnamedtupleNamedTupleCard=namedtuple('NamedTupleCard',['rank','suit'])This definition ofNamedTupleCard will give the exact same output as ourDataClassCard example did:
>>>queen_of_hearts=NamedTupleCard('Q','Hearts')>>>queen_of_hearts.rank'Q'>>>queen_of_heartsNamedTupleCard(rank='Q', suit='Hearts')>>>queen_of_hearts==NamedTupleCard('Q','Hearts')TrueSo why even bother with data classes? First of all, data classes come with many more features than you have seen so far. At the same time, thenamedtuple has some other features that are not necessarily desirable. By design, anamedtuple is a regular tuple. This can be seen in comparisons, for instance:
>>>queen_of_hearts==('Q','Hearts')TrueWhile this might seem like a good thing, this lack of awareness about its own type can lead to subtle and hard-to-find bugs, especially since it will also happily compare two differentnamedtuple classes:
>>>Person=namedtuple('Person',['first_initial','last_name']>>>ace_of_spades=NamedTupleCard('A','Spades')>>>ace_of_spades==Person('A','Spades')TrueThenamedtuple also comes with some restrictions. For instance, it is hard to add default values to some of the fields in anamedtuple. Anamedtuple is also by natureimmutable. That is, the value of anamedtuple can never change. In some applications, this is an awesome feature, but in other settings, it would be nice to have more flexibility:
>>>card=NamedTupleCard('7','Diamonds')>>>card.rank='9'AttributeError: can't set attributeData classes will not replace all uses ofnamedtuple. For instance, if you need your data structure to behave like a tuple, then a named tuple is a great alternative!
Another alternative, and one of theinspirations for data classes, is theattrs project. Withattrs installed (pip install attrs), you can write a card class as follows:
importattr@attr.sclassAttrsCard:rank=attr.ib()suit=attr.ib()This can be used in exactly the same way as theDataClassCard andNamedTupleCard examples earlier. Theattrs project is great and does support some features that data classes do not, including converters and validators. Furthermore,attrs has been around for a while and is supported in Python 2.7 as well as Python 3.4 and up. However, asattrs is not a part of the standard library, it does add an externaldependency to your projects. Through data classes, similar functionality will be available everywhere.
In addition totuple,dict,namedtuple, andattrs, there aremany other similar projects, includingtyping.NamedTuple,namedlist,attrdict,plumber, andfields. While data classes are a great alternative, there are still use cases where one of the other variants fits better. For instance, if you need compatibility with a specific API expecting tuples or need functionality not supported in data classes.
Basic Data Classes
Let us get back to data classes. As an example, we will create aPosition class that will represent geographic positions with a name as well as the latitude and longitude:
fromdataclassesimportdataclass@dataclassclassPosition:name:strlon:floatlat:floatWhat makes this a data class is the@dataclass decorator just above the class definition. Beneath theclass Position: line, you simply list the fields you want in your data class. The: notation used for the fields is using a feature calledvariable annotations. We willsoon talk more about this notation and why we specify data types likestr andfloat.
Those few lines of code are all you need. The new class is ready for use:
>>>pos=Position('Oslo',10.8,59.9)>>>print(pos)Position(name='Oslo', lon=10.8, lat=59.9)>>>pos.lat59.9>>>print(f'{pos.name} is at{pos.lat}°N,{pos.lon}°E')Oslo is at 59.9°N, 10.8°EYou can also create data classes similarly to how named tuples are created. The following is (almost) equivalent to the definition ofPosition above:
fromdataclassesimportmake_dataclassPosition=make_dataclass('Position',['name','lat','lon'])A data class is a regularPython class. The only thing that sets it apart is that it has basicdata model methods like.__init__(),.__repr__(), and.__eq__() implemented for you.
Default Values
It is easy to add default values to the fields of your data class:
fromdataclassesimportdataclass@dataclassclassPosition:name:strlon:float=0.0lat:float=0.0This works exactly as if you had specified the default values in the definition of the.__init__() method of a regular class:
>>>Position('Null Island')Position(name='Null Island', lon=0.0, lat=0.0)>>>Position('Greenwich',lat=51.8)Position(name='Greenwich', lon=0.0, lat=51.8)>>>Position('Vancouver',-123.1,49.3)Position(name='Vancouver', lon=-123.1, lat=49.3)Later you will learn aboutdefault_factory, which gives a way to provide more complicated default values.
Type Hints
So far, we have not made a big fuss of the fact that data classes supporttyping out of the box. You have probably noticed that we defined the fields with a type hint:name: str says thatname should be atext string (str type).
In fact, adding some kind of type hint is mandatory when defining the fields in your data class. Without a type hint, the field will not be a part of the data class. However, if you do not want to add explicit types to your data class, usetyping.Any:
fromdataclassesimportdataclassfromtypingimportAny@dataclassclassWithoutExplicitTypes:name:Anyvalue:Any=42While you need to add type hints in some form when using data classes, these types are not enforced at runtime. The following code runs without any problems:
>>>Position(3.14,'pi day',2018)Position(name=3.14, lon='pi day', lat=2018)This is how typing in Python usually works:Python is and will always be a dynamically typed language. To actually catch type errors, type checkers likeMypy can be run on your source code.
Adding Methods
You already know that a data class is just a regular class. That means that you can freely add your own methods to a data class. As an example, let us calculate the distance between one position and another, along the Earth’s surface. One way to do this is by usingthe haversine formula:

You can add a.distance_to() method to your data class just like you can with normal classes:
fromdataclassesimportdataclassfrommathimportasin,cos,radians,sin,sqrt@dataclassclassPosition:name:strlon:float=0.0lat:float=0.0defdistance_to(self,other):r=6371# Earth radius in kilometerslam_1,lam_2=radians(self.lon),radians(other.lon)phi_1,phi_2=radians(self.lat),radians(other.lat)h=(sin((phi_2-phi_1)/2)**2+cos(phi_1)*cos(phi_2)*sin((lam_2-lam_1)/2)**2)return2*r*asin(sqrt(h))It works as you would expect:
>>>oslo=Position('Oslo',10.8,59.9)>>>vancouver=Position('Vancouver',-123.1,49.3)>>>oslo.distance_to(vancouver)7181.7841229421165More Flexible Data Classes
So far, you have seen some of the basic features of the data class: it gives you some convenience methods, and you can still add default values and other methods. Now you will learn about some more advanced features like parameters to the@dataclass decorator and thefield() function. Together, they give you more control when creating a data class.
Let us return to the playing card example you saw at the beginning of the tutorial and add a class containing a deck of cards while we are at it:
fromdataclassesimportdataclassfromtypingimportList@dataclassclassPlayingCard:rank:strsuit:str@dataclassclassDeck:cards:List[PlayingCard]A simple deck containing only two cards can be created like this:
>>>queen_of_hearts=PlayingCard('Q','Hearts')>>>ace_of_spades=PlayingCard('A','Spades')>>>two_cards=Deck([queen_of_hearts,ace_of_spades])Deck(cards=[PlayingCard(rank='Q', suit='Hearts'), PlayingCard(rank='A', suit='Spades')])Advanced Default Values
Say that you want to give a default value to theDeck. It would for example be convenient ifDeck() created aregular (French) deck of 52 playing cards. First, specify the different ranks and suits. Then, add a functionmake_french_deck() that creates alist of instances ofPlayingCard:
RANKS='2 3 4 5 6 7 8 9 10 J Q K A'.split()SUITS='♣ ♢ ♡ ♠'.split()defmake_french_deck():return[PlayingCard(r,s)forsinSUITSforrinRANKS]For fun, the four different suits are specified using theirUnicode symbols.
Note: Above, we used Unicode glyphs like
♠directly in the source code. We could do this becausePython supports writing source code in UTF-8 by default. Refer tothis page on Unicode input for how to enter these on your system. You could also enter the Unicode symbols for the suits using\Nnamed character escapes (like\N{BLACK SPADE SUIT}) or\uUnicode escapes (like\u2660).
To simplify comparisons of cards later, the ranks and suits are also listed in their usual order.
>>>make_french_deck()[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ... PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')]In theory, you could now use this function to specify a default value forDeck.cards:
fromdataclassesimportdataclassfromtypingimportList@dataclassclassDeck:# Will NOT workcards:List[PlayingCard]=make_french_deck()Don’t do this! This introduces one of the most common anti-patterns in Python:using mutable default arguments. The problem is that all instances ofDeck will use the same list object as the default value of the.cards property. This means that if, say, one card is removed from oneDeck, then it disappears from all other instances ofDeck as well. Actually, data classes try toprevent you from doing this, and the code above will raise aValueError.
Instead, data classes use something called adefault_factory to handle mutable default values. To usedefault_factory (and many other cool features of data classes), you need to use thefield() specifier:
fromdataclassesimportdataclass,fieldfromtypingimportList@dataclassclassDeck:cards:List[PlayingCard]=field(default_factory=make_french_deck)The argument todefault_factory can be any zero parameter callable. Now it is easy to create a full deck of playing cards:
>>>Deck()Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ... PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])Thefield() specifier is used to customize each field of a data class individually. You will see some other examples later. For reference, these are the parametersfield() supports:
default: Default value of the fielddefault_factory: Function that returns the initial value of the fieldinit: Use field in.__init__()method? (Default isTrue.)repr: Use field inreprof the object? (Default isTrue.)compare: Include the field in comparisons? (Default isTrue.)hash: Include the field when calculatinghash()? (Default is to use the same as forcompare.)metadata: A mapping with information about the field
In thePosition example, you saw how to add simple default values by writinglat: float = 0.0. However, if you also want to customize the field, for instance to hide it in therepr, you need to use thedefault parameter:lat: float = field(default=0.0, repr=False). You may not specify bothdefault anddefault_factory.
Themetadata parameter is not used by the data classes themselves but is available for you (or third party packages) to attach information to fields. In thePosition example, you could for instance specify that latitude and longitude should be given in degrees:
fromdataclassesimportdataclass,field@dataclassclassPosition:name:strlon:float=field(default=0.0,metadata={'unit':'degrees'})lat:float=field(default=0.0,metadata={'unit':'degrees'})The metadata (and other information about a field) can be retrieved using thefields() function (note the plurals):
>>>fromdataclassesimportfields>>>fields(Position)(Field(name='name',type=<class 'str'>,...,metadata={}), Field(name='lon',type=<class 'float'>,...,metadata={'unit': 'degrees'}), Field(name='lat',type=<class 'float'>,...,metadata={'unit': 'degrees'}))>>>lat_unit=fields(Position)[2].metadata['unit']>>>lat_unit'degrees'You Need Representation?
Recall that we can create decks of cards out of thin air:
>>>Deck()Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ... PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])While this representation of aDeck is explicit and readable, it is also very verbose. I have deleted 48 of the 52 cards in the deck in the output above. On an 80-column display, simply printing the fullDeck takes up 22 lines! Let us add a more concise representation. In general, a Python object hastwo different string representations:
repr(obj)is defined byobj.__repr__()and should return a developer-friendly representation ofobj. If possible, this should be code that can recreateobj. Data classes do this.str(obj)is defined byobj.__str__()and should return a user-friendly representation ofobj. Data classes do not implement a.__str__()method, so Python will fall back to the.__repr__()method.
Let us implement a user-friendly representation of aPlayingCard:
fromdataclassesimportdataclass@dataclassclassPlayingCard:rank:strsuit:strdef__str__(self):returnf'{self.suit}{self.rank}'The cards now look much nicer, but the deck is still as verbose as ever:
>>>ace_of_spades=PlayingCard('A','♠')>>>ace_of_spadesPlayingCard(rank='A', suit='♠')>>>print(ace_of_spades)♠A>>>print(Deck())Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ... PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])To show that it is possible to add your own.__repr__() method as well, we will violate the principle that it should return code that can recreate an object.Practicality beats purity after all. The following code adds a more concise representation of theDeck:
fromdataclassesimportdataclass,fieldfromtypingimportList@dataclassclassDeck:cards:List[PlayingCard]=field(default_factory=make_french_deck)def__repr__(self):cards=', '.join(f'{c!s}'forcinself.cards)returnf'{self.__class__.__name__}({cards})'Note the!s specifier in the{c!s} format string. It means that we explicitly want to use thestr() representation of eachPlayingCard. With the new.__repr__(), the representation ofDeck is easier on the eyes:
>>>Deck()Deck(♣2, ♣3, ♣4, ♣5, ♣6, ♣7, ♣8, ♣9, ♣10, ♣J, ♣Q, ♣K, ♣A, ♢2, ♢3, ♢4, ♢5, ♢6, ♢7, ♢8, ♢9, ♢10, ♢J, ♢Q, ♢K, ♢A, ♡2, ♡3, ♡4, ♡5, ♡6, ♡7, ♡8, ♡9, ♡10, ♡J, ♡Q, ♡K, ♡A, ♠2, ♠3, ♠4, ♠5, ♠6, ♠7, ♠8, ♠9, ♠10, ♠J, ♠Q, ♠K, ♠A)This is a nicer representation of the deck. However, it comes at a cost. You’re no longer able to recreate the deck by executing its representation. Often, you’d be better off implementing the same representation with.__str__() instead.
Comparing Cards
In many card games, cards are compared to each other. For instance in a typical trick taking game, the highest card takes the trick. As it is currently implemented, thePlayingCard class does not support this kind of comparison:
>>>queen_of_hearts=PlayingCard('Q','♡')>>>ace_of_spades=PlayingCard('A','♠')>>>ace_of_spades>queen_of_heartsTypeError: '>' not supported between instances of 'Card' and 'Card'This is, however, (seemingly) easy to rectify:
fromdataclassesimportdataclass@dataclass(order=True)classPlayingCard:rank:strsuit:strdef__str__(self):returnf'{self.suit}{self.rank}'The@dataclass decorator has two forms. So far you have seen the simple form where@dataclass is specified without any parentheses and parameters. However, you can also give parameters to the@dataclass() decorator in parentheses. The following parameters are supported:
init: Add.__init__()method? (Default isTrue.)repr: Add.__repr__()method? (Default isTrue.)eq: Add.__eq__()method? (Default isTrue.)order: Add ordering methods? (Default isFalse.)unsafe_hash: Force the addition of a.__hash__()method? (Default isFalse.)frozen: IfTrue, assigning to fields raise an exception. (Default isFalse.)
Seethe original PEP for more information about each parameter. After settingorder=True, instances ofPlayingCard can be compared:
>>>queen_of_hearts=PlayingCard('Q','♡')>>>ace_of_spades=PlayingCard('A','♠')>>>ace_of_spades>queen_of_heartsFalseHow are the two cards compared though? You have not specified how the ordering should be done, and for some reason Python seems to believe that a Queen is higher than an Ace…
It turns out that data classes compare objects as if they were tuples of their fields. In other words, a Queen is higher than an Ace because'Q' comes after'A' in the alphabet:
>>>('A','♠')>('Q','♡')FalseThat does not really work for us. Instead, we need to define some kind of sort index that uses the order ofRANKS andSUITS. Something like this:
>>>RANKS='2 3 4 5 6 7 8 9 10 J Q K A'.split()>>>SUITS='♣ ♢ ♡ ♠'.split()>>>card=PlayingCard('Q','♡')>>>RANKS.index(card.rank)*len(SUITS)+SUITS.index(card.suit)42ForPlayingCard to use this sort index for comparisons, we need to add a field.sort_index to the class. However, this field should be calculated from the other fields.rank and.suit automatically. This is exactly what thespecial method.__post_init__() is for. It allows for special processing after the regular.__init__() method is called:
fromdataclassesimportdataclass,fieldRANKS='2 3 4 5 6 7 8 9 10 J Q K A'.split()SUITS='♣ ♢ ♡ ♠'.split()@dataclass(order=True)classPlayingCard:sort_index:int=field(init=False,repr=False)rank:strsuit:strdef__post_init__(self):self.sort_index=(RANKS.index(self.rank)*len(SUITS)+SUITS.index(self.suit))def__str__(self):returnf'{self.suit}{self.rank}'Note that.sort_index is added as the first field of the class. That way, the comparison is first done using.sort_index and only if there are ties are the other fields used. Usingfield(), you must also specify that.sort_index should not be included as a parameter in the.__init__() method (because it is calculated from the.rank and.suit fields). To avoid confusing the user about this implementation detail, it is probably also a good idea to remove.sort_index from therepr of the class.
Finally, aces are high:
>>>queen_of_hearts=PlayingCard('Q','♡')>>>ace_of_spades=PlayingCard('A','♠')>>>ace_of_spades>queen_of_heartsTrueYou can now easily create a sorted deck:
>>>Deck(sorted(make_french_deck()))Deck(♣2, ♢2, ♡2, ♠2, ♣3, ♢3, ♡3, ♠3, ♣4, ♢4, ♡4, ♠4, ♣5, ♢5, ♡5, ♠5, ♣6, ♢6, ♡6, ♠6, ♣7, ♢7, ♡7, ♠7, ♣8, ♢8, ♡8, ♠8, ♣9, ♢9, ♡9, ♠9, ♣10, ♢10, ♡10, ♠10, ♣J, ♢J, ♡J, ♠J, ♣Q, ♢Q, ♡Q, ♠Q, ♣K, ♢K, ♡K, ♠K, ♣A, ♢A, ♡A, ♠A)Or, if you don’t care aboutsorting, this is how you draw a random hand of 10 cards:
>>>fromrandomimportsample>>>Deck(sample(make_french_deck(),k=10))Deck(♢2, ♡A, ♢10, ♣2, ♢3, ♠3, ♢A, ♠8, ♠9, ♠2)Of course, you don’t needorder=True for that…
Immutable Data Classes
One of the defining features of thenamedtuple you saw earlier is that it isimmutable. That is, the value of its fields may never change. For many types of data classes, this is a great idea! To make a data class immutable, setfrozen=True when you create it. For example, the following is an immutable version of thePosition classyou saw earlier:
fromdataclassesimportdataclass@dataclass(frozen=True)classPosition:name:strlon:float=0.0lat:float=0.0In a frozen data class, you can not assign values to the fields after creation:
>>>pos=Position('Oslo',10.8,59.9)>>>pos.name'Oslo'>>>pos.name='Stockholm'dataclasses.FrozenInstanceError: cannot assign to field 'name'Be aware though that if your data class contains mutable fields, those might still change. This is true for all nested data structures in Python (seethis video for further info):
fromdataclassesimportdataclassfromtypingimportList@dataclass(frozen=True)classImmutableCard:rank:strsuit:str@dataclass(frozen=True)classImmutableDeck:cards:List[ImmutableCard]Even though bothImmutableCard andImmutableDeck are immutable, the list holdingcards is not. You can therefore still change the cards in the deck:
>>>queen_of_hearts=ImmutableCard('Q','♡')>>>ace_of_spades=ImmutableCard('A','♠')>>>deck=ImmutableDeck([queen_of_hearts,ace_of_spades])>>>deckImmutableDeck(cards=[ImmutableCard(rank='Q', suit='♡'), ImmutableCard(rank='A', suit='♠')])>>>deck.cards[0]=ImmutableCard('7','♢')>>>deckImmutableDeck(cards=[ImmutableCard(rank='7', suit='♢'), ImmutableCard(rank='A', suit='♠')])To avoid this, make sure all fields of an immutable data class use immutable types (but remember that types are not enforced at runtime). TheImmutableDeck should be implemented using a tuple instead of a list.
Inheritance
You cansubclass data classes quite freely. As an example, we will extend ourPosition example with acountry field and use it to record capitals:
fromdataclassesimportdataclass@dataclassclassPosition:name:strlon:floatlat:float@dataclassclassCapital(Position):country:strIn this simple example, everything works without a hitch:
>>>Capital('Oslo',10.8,59.9,'Norway')Capital(name='Oslo', lon=10.8, lat=59.9, country='Norway')Thecountry field ofCapital is added after the three original fields inPosition. Things get a little more complicated if any fields in the base class have default values:
fromdataclassesimportdataclass@dataclassclassPosition:name:strlon:float=0.0lat:float=0.0@dataclassclassCapital(Position):country:str# Does NOT workThis code will immediately crash with aTypeError complaining that “non-default argument ‘country’ follows default argument.” The problem is that our newcountry field has no default value, while thelon andlat fields have default values. The data class will try to write an.__init__() method with the following signature:
def__init__(name:str,lon:float=0.0,lat:float=0.0,country:str):...However, this is not valid Python.If a parameter has a default value, all following parameters must also have a default value. In other words, if a field in a base class has a default value, then all new fields added in a subclass must have default values as well.
Another thing to be aware of is how fields are ordered in a subclass. Starting with the base class, fields are ordered in the order in which they are first defined. If a field is redefined in a subclass, its order does not change. For example, if you definePosition andCapital as follows:
fromdataclassesimportdataclass@dataclassclassPosition:name:strlon:float=0.0lat:float=0.0@dataclassclassCapital(Position):country:str='Unknown'lat:float=40.0Then the order of the fields inCapital will still bename,lon,lat,country. However, the default value oflat will be40.0.
>>>Capital('Madrid',country='Spain')Capital(name='Madrid', lon=0.0, lat=40.0, country='Spain')Optimizing Data Classes
I’m going to end this tutorial with a few words aboutslots. Slots can be used to make classes faster and use less memory. Data classes have no explicit syntax for working with slots, but the normal way of creating slots works for data classes as well. (They really are just regular classes!)
fromdataclassesimportdataclass@dataclassclassSimplePosition:name:strlon:floatlat:float@dataclassclassSlotPosition:__slots__=['name','lon','lat']name:strlon:floatlat:floatEssentially, slots are defined using.__slots__ to list the variables on a class. Variables or attributes not present in.__slots__ may not be defined. Furthermore, a slots class may not have default values.
The benefit of adding such restrictions is that certain optimizations may be done. For instance, slots classes take up less memory, as can be measured usingPympler:
>>>frompymplerimportasizeof>>>simple=SimplePosition('London',-0.1,51.5)>>>slot=SlotPosition('Madrid',-3.7,40.4)>>>asizeof.asizesof(simple,slot)(440, 248)Similarly, slots classes are typically faster to work with. The following example measures the speed of attribute access on a slots data class and a regular data class usingtimeit from the standard library.
>>>fromtimeitimporttimeit>>>timeit('slot.name',setup="slot=SlotPosition('Oslo', 10.8, 59.9)",globals=globals())0.05882283499886398>>>timeit('simple.name',setup="simple=SimplePosition('Oslo', 10.8, 59.9)",globals=globals())0.09207444800267695In this particular example, the slot class is about 35% faster.
Conclusion & Further Reading
Data classes are a convenient feature of Python. With data classes, you don’t have to write boilerplate code to get proper initialization, representation, and comparisons for your objects.
In this tutorial, you’ve learned how to define your own data classes, as well as:
- How to add default values to the fields in your data class
- How to customize the ordering of data class objects
- How to work with immutable data classes
- How inheritance works for data classes
If you want to dive into all the details of data classes, have a look atPEP 557 as well as the discussions in the originalGitHub repo.
In addition, Raymond Hettinger’s PyCon 2018 talkDataclasses: The code generator to end all code generators is well worth watching. And now, go forth and write less code!
Take the Quiz: Test your knowledge with our interactive “Data Classes in Python” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Data Classes in PythonIn this quiz, you'll test your understanding of Python data classes.Data classes, a feature introduced in Python 3.7, are a type of class mainly used for storing data.They come with basic functionality already implemented, such as instance initialization, printing, and comparison.
Recommended Course
🐍 Python Tricks 💌
Get a short & sweetPython Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

AboutGeir Arne Hjelle
Geir Arne is an avid Pythonista and a member of the Real Python tutorial team.
» More about Geir ArneMasterReal-World Python Skills With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
MasterReal-World Python Skills
With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
What Do You Think?
What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.
Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students.Get tips for asking good questions andget answers to common questions in our support portal.
Looking for a real-time conversation? Visit theReal Python Community Chat or join the next“Office Hours” Live Q&A Session. Happy Pythoning!
Keep Learning
Keep reading Real Python by creating a free account or signing in:
Already have an account?Sign-In





