The Properties of Python's `property`
Exploring Python's properties through my return to athletics training
There are some English words I aim to avoid when writing about Python. These are perfectly normal words that have a well-known English meaning that can be useful when explaining concepts. But the same words also have a specific meaning in Python.
Object is an obvious one. When talking about Python, I avoid using this in its 'plain English' sense. A harder one to fight off is instance – it's tempting to use "in this instance" when explaining concepts. I now use "in this case" instead. Sure, case
is itself a Python keyword, but I'm OK with that compromise.
Property is another one. Not the real estate type – I don't need to talk about houses often when talking about Python. But the other meaning of property. Something's property is its attribute – but no, that doesn't work either! So, I use "characteristic" instead.
But this article is about properties in Python. And this time, I really mean property!
Here's something you probably don't know about me. In my youth, I was a serious athlete – track and field – the nine-training-sessions-a-week type. I even represented my native Malta in junior international competitions. But I stopped in my early twenties for reasons I won't bore you with.
That was then…
But I recently joined an athletics club again. My son joined, too – he's roughly the same age I was when I got serious about athletics. I started training again. Hopefully, I'll compete soon – a quarter of a century after my last competition.
Right. Allow me to branch off from real life into fiction now. The story so far was real. The rest isn't
One of the first people I met in my new club was Todd. He's a promising young athlete and a budding Python programmer. Todd recognised me from one of the videos his coding teacher uses at school – one of my Python videos. [This really happens occasionally in real life. My five minutes of fame!]
He asked me to look at the code he wrote to help the club manage their athletes. My club is a small, local club. Resources are limited.
Note: the next section deals with Todd's original code, which didn't include properties. It sets the scene for the sections on properties. If you're not in a rush, I'd recommend reading this section. However, if you're impatient to get to the bit on properties, you can jump to the "Problem" sections: "The .athlete_id
Problem", "The .age
and .age_group
Problem", and "The .name
Problem".
Todd's Initial Code
Here's Todd's code that deals with club members. We'll go through this code section by section below:

Let's explore Todd's code.
The Two Enum Classes: AgeGroup
and Event
Todd starts off with two enum classes. Enums are lightweight classes that create constant-like members. These constants are contained within their own namespaces. Therefore, instead of using strings to represent constant values such as age groups or athletic events, you can use AgeGroup.U20
or Event.T_400M
, say. Each member of an enum has a name and a value. And you can use .name
and .value
to access these:
The comments here and throughout the article show the outputs from the print()
calls.
The values of each AgeGroup
enum represent the upper end of each age group. This upper limit is excluded from the age group, which matches the default for Python number ranges in general. Each age group's lower limit is represented by the value of its preceding member.
Note that, unlike with standard classes, you don't create instances of the enum class by using the class name followed by parentheses. All you need is already there–the members defined within the class.
I won't dwell on enums as this article is about another topic. But you'll need to know that you can iterate directly through the enum class – yes the class itself, not an instance of the class:
You may have noticed that AgeGroup
is an IntEnum
. The members of IntEnum
classes are themselves integers. Therefore, you can use the members directly as their integer values rather than relying on the .values
attribute:
You'd have to use AgeGroup.U20.value + 5
if AgeGroup
was a standard Enum
instead of an IntEnum
.
The ClubMember
Class's .__init__()
, .__str__()
, and .__repr__()
Special Methods
Todd defines the ClubMember
class next. The .__init__()
special method does what .__init__()
methods always do – it initialises an instance of the class. This method defines six data attributes, three of which are passed as arguments when you create an instance of the class:
.name
is the athlete's full name. We'll revisit this data attribute later in this article as we discuss improvements to Todd's code..dob
is the athlete's date of birth. Todd doesn't include documentation in his code, and he doesn't use type hints, which makes it harder to figure out what type of data.dob
ought to be. Exploring the code further, you'll see it needs to be adatetime.datetime
object (adatetime.date
, more accurately).But if you look a bit further down in the
ClubMember
class, you'll notice Todd defines a static method. A static method doesn't need information about the instance or the class and, therefore, doesn't haveself
orcls
as its first parameter. This is the.get_dob_from_string()
method, which converts a string to adatetime.date
object. This method makes it easy to obtain the requireddatetime.date
object you need to create aClubMember
instance.Todd also calculates the age from the date of birth. The calculation first takes the difference between the current year and the year of birth. Then, it accounts for whether the present day is before or after the athlete's birthday. The algorithm subtracts
1
if the athlete still hasn't had his or her birthday in the current year.Todd does this by comparing two tuples. One has the current month and day. The other has the month and day of the athlete's birthday. When Python compares tuples, it first compares the first elements of each tuple. If they're equal, it moves on to compare the second elements.
But the less than operator,
<
, returnsTrue
orFalse
. However, Boolean values are integer subtypes. So, you can subtractTrue
from an integer, which subtracts1
from the integer..athlete_id
is the athlete's registration number. We'll also discuss this later as we improve Todd's code..age_group
stores the athlete's age group, which is one of theAgeGroup
enum members. We'll discuss the.calculate_age_group()
later in this article. Todd uses this method to work out the age group..events
is a list that stores – you guessed it – the athlete's track and field specialities. It starts off as an empty list. We'll discuss Todd's method to add events later.
Todd also defines .__str__()
and .__repr__()
special methods to cater for both types of string representations. Let's create an instance for a young, up-and-coming athlete and display the two string representations:
The .__str__()
output shows the athlete's name and age group. The .__repr__()
output conforms to the convention for this type of representation and displays a string that allows you to create an identical instance when evaluated.
The .calculate_age_group()
Method
The .calculate_age_group()
method is a helper method that Todd calls when defining the .age_group
data attribute. This method relies on an enum characteristic: the class itself is iterable. You can use the class directly in a for
loop to iterate over all its members.
Todd defines the age groups from youngest to oldest, and he sets their numeric values to the (exclusive) upper limit of the age group's age range. Therefore, .calculate_age_group()
iterates through the age groups and compares the athlete's age with each age group's upper limit. The method returns the first age group for which the athlete's age is lower than the age group's upper limit.
Recall that a function or method stops running the moment it returns a value. Therefore, the for
loop may not need to iterate through all the AgeGroup
enum members.
Did you spot what Todd does for the AgeGroup.MASTER
age group? Since there's no upper limit for the older group – this age group includes anyone 35 years or older – Todd sets its value to -1
. Since no athlete's age can be lower than -1
, this value ensures that this age group is effectively excluded from the for
loop, which has an if
statement checking whether the athlete's age is lower than the enum member's value.
If the for
loop finishes iterating without assigning an athlete to an age group, then this athlete must be an older one. Therefore, the method returns the AgeGroup.MASTER
age group.
You can experiment with the athlete's date of birth to ensure that all age groups are dealt with correctly.
The .add_events()
and .display_events()
Methods
There are two methods left to discuss. The first of these, .add_events()
, does what it says in its name. You call this method with any number of Event
enum members as arguments since the method's parameter, *events_to_add
, has a leading *
. This type of parameter is often referred to as *args
, and it allows any number of arguments to be used when calling the method.
The method checks that all elements passed are of type Event
, the enum Todd creates at the top of his code. Recall that the parameter *events_to_add
in the method definition creates a tuple called events_to_add
. Therefore, you can extend the list .events
using the elements of events_to_add
.
And .display_events()
uses a generator expression – this is the statement that looks like a list comprehension but is enclosed in parentheses instead of square brackets – and unpacks it using the unpacking operator *
. This is equivalent to passing the events listed in .events
separately as arguments in print()
.
Let's try these methods:
You can try passing the wrong data types to .add_events()
to ensure the data type validation works.
Todd had done a great job. But he had a few problems with his code. So, I sat down with him to find solutions.
The .athlete_id
Problem
Each athlete registers with the club, but they also register with the national association, which gives them a unique registration number. This made things easy for Todd as the .athlete_id
is the number supplied by the national association. So, he doesn't need to create an id number and ensure each athlete gets a unique number.
However, the club had an incident a few weeks ago. Todd's code had changed a club member's .athlete_id
by mistake, and they didn't realise this happened until the athlete's registration for a race was rejected because of an invalid registration number.
Since the club was already using this software, Todd and I agreed we didn't want to make changes that break how the club is already using the code, if possible. Since the code already has an .athlete_id
data attribute that's used in many places in the code, we chose not to change this.
Python's Property
So, I suggested making .athlete_id
read-only so that it can't be changed accidentally.
To do this, we can change .athlete_id
from a data attribute into a property:
There are two changes to the code:
We renamed the data attribute defined in the
.__init__()
method to._athlete_id
, with a leading underscore. The leading underscore identifies this attribute as non-public. It's not really a private attribute that cannot be accessed from outside the class – Python doesn't have private attributes – but it clearly shows the programmer's intent to any user of this class: this attribute is not meant to be accessed from outside the class. Many IDEs will not volunteer this attribute as an autocomplete option outside a class definition. And it's a good practice to avoid using attributes with leading underscores outside of class definitions.We then defined a new method called
athlete_id
. This is the same name as the old data attribute that we just replaced. However, this method is decorated with the@property
decorator. [Psst! There's a series about decorators coming soon here at The Python Coding Stack.] Therefore, the nameathlete_id
is a property. We'll see what this means shortly.The
athlete_id
method, which is now a property, returns the value of the._athlete_id
data attribute – the one with the leading underscore.
Let's see how this works before explaining what a property is:
You use .athlete_id
as if it were a data attribute. Note that you don't include parentheses after .athlete_id
. In fact, anyone using this class doesn't need to know that .athlete_id
is not an actual data attribute. A property looks and feels like a data attribute, but it's really a method.
The method athlete_id
that's decorated with the @property
decorator is the getter for this attribute. It gets the attribute's value.
The value is stored in the non-public ._athlete_id
data attribute internally within the class, but it's accessed through the .athlete_id
property, which uses the getter method.
A getter gets the value of an attribute. But it doesn't allow you to set a new value:
The code raises an error when you try to set a new value for .athlete_id
:
Traceback (most recent call last):
File "...", line 89, in <module>
mj.athlete_id = 258762612
^^^^^^^^^^^^^
AttributeError: property 'athlete_id' of 'ClubMember'
object has no setter
The property .athlete_id
is a read-only attribute. You can access its value, but you can't change it.
Note that, as mentioned earlier, Python doesn't have private attributes. Therefore, a determined user can still change the value of the athlete's registration number by accessing the non-public data attribute ._athlete_id
directly. However, you shouldn't be accessing attributes with leading underscores in your code. If you do so, you're doing this off-label and you can expect nasty consequences!
But There Isn't Always a Registration Number When We First Create an Athlete Instance
This solution solves Todd's problem. Now, the code cannot override an athlete's registration number by mistake.
However, Todd then pointed out that often, when they first add an athlete to the club, they don't have a registration number. This is assigned later when they register the athlete with the national association. So, it should be possible to assign a value to .athlete_id
, but the code should only be able to do so once.
To control what happens when you assign a value to a data attribute, you can use a property! But wait a second, .athlete_id
is already a property. That's what we just discussed in the previous section.
However, the property only has a getter at the moment. Now, it needs a setter, too. But it needs a setter that only allows you to assign a registration number once:
You define another method using the same name, athlete_id
. Since you already decorated the getter as a property, you now decorate this second method using @athlete_id.setter
. This is the setter method for the .athlete_id
property. You can now set its value.
However, the setter method first checks whether the value of the non-public ._athlete_id
, the one with the leading underscore, is None
. Recall that None
is the default value for ._athlete_id
since you define this default value in the .__init__()
signature.
If ._athlete_id
is not None
, then it means that there's already a registration number present. The code raises an error. However, if the if
statement doesn't raise the error, then you set ._athlete_id
to the new value.
The code above still assigns the registration number when you first create the mj
instance. Therefore, the final line, which tries to assign a new registration number to our young Michael Johnson, raises an error:
Traceback (most recent call last):
File "...", line 97, in <module>
mj.athlete_id = 258762612
^^^^^^^^^^^^^
File "...", line 58, in athlete_id
raise ValueError(
"You can't change the value of 'athlete_id'"
)
ValueError: You can't change the value of 'athlete_id'
This time, the error message is a friendlier one – one you define in the setter method.
Let's ensure you can set the registration number if it contains None
:
Note that you don't pass a third argument when creating an instance of ClubMember
this time – the line with the third argument is commented out.
The first time you get the value of the .athlete_id
property, the code returns None
. And now, you can assign a registration value to the athlete.
However, you can only do so once:
The final line, which attempts to reassign a new registration number, now raises an error:
ValueError: You can't change the value of 'athlete_id'
Todd doesn't need to change anything else in the code. Anywhere he's using .athlete_id
in his code, which used to be a data attribute, still works as it's now a property with a getter and a setter. The setter checks that you're not trying to overwrite an existing registration number.
Do you want to join a forum to discuss Python further with other Pythonistas? Upgrade to a paid subscription here on The Python Coding Stack to get exclusive access to The Python Coding Place's members' forum. More Python. More discussions. More fun.
And you'll also be supporting this publication. I put plenty of time and effort into crafting each article. Your support will help me keep this content coming regularly and, importantly, will help keep it free for everyone.
The .age
and .age_group
Problem
One problem resolved, but there's another issue that Todd hasn't spotted yet. Can you spot it? It's related to the athletes' ages and age groups.
The club had only been using Todd's code for a few months, which is why this problem hasn't surfaced yet. Look again at the code if you haven't spotted the problem. Then, read on…
The athlete's .age
and .age_group
data attributes are set in the .__init__()
method, which works out an athlete's age and then uses the age to assign them to an age group. The code calls this method when it creates an instance of the class.
But people get older (despite attempts by some of us to stop the passage of time.) Todd's code freezes an athlete's age and age group to whatever values they had when the athlete joined the club (or, more accurately, when the ClubMember
instance was created.)
We need the .age
and .age_group
data attributes to be calculated each time we use them to make sure they're still correct.
Ideally, Todd needs to define methods to calculate these values. He already has one of them, .calculate_age_group()
, but he's only using this method in the .__init__()
special method. Elsewhere in his code, Todd is accessing the data attribute .age_group
directly. Can he avoid replacing lots of lines in this code, which may lead to more bugs?
And he needs to do the same with .age
by creating a method to calculate the athlete's age and then use the method instead of the .age
data attribute everywhere in his code.
It can be done. But there's an easier and safer way to avoid lots of refactoring throughout the code.
Replacing The .age
Data Attribute With a .age
Property
The solution is to create a property. Todd already has a .age
data attribute. He can delete this data attribute and create a .age
property instead:
The .age
data attribute is no longer in .__init__()
. Instead, Todd and I defined a property – a method decorated with @property
. This method is the getter function. It allows you to get the value of .age
. Instead of retrieving a stored value, it calculates it each time it's needed.
Compare this with the .athlete_id
property from the previous section. The .athlete_id
property needed a non-public ._athlete_id
data attribute to store the value. The property provided a way of accessing this value. However, .age
doesn't need its own data attribute since it calculates the value it needs each time using other values: the current date and the value stored in the .dob
attribute.
The code in the .age
getter is the same code Todd had in the .__init__()
method earlier. This property doesn't need a setter. It's a read-only property. A user can change the athlete's date of birth if needed, and that change will be reflected the next time you fetch .age
.
And you know what will happen if you try to change the .age
attribute:
This code raises an error:
AttributeError: property 'age' of 'ClubMember'
object has no setter
Converting .age_group
Into a Property
Once we finished refactoring the code to fix the .age
problem, Todd went ahead and suggested a similar solution for .age_group
. The age group needs to be updated when the age changes. Therefore, Todd decided to change this data attribute to a property.
However, he already has the method he needs in place: .calculate_age_group()
. And he's only using this method in the .__init__()
method. So, Todd deleted the .age_group
data attribute from .__init__()
and renamed the .calculate_age_group()
method to .age_group()
. He also decorated this method with @property
:
Let's check everything still works:
Todd can still use .age_group
in this code. Instead of being a data attribute, it's now a property. And if you change the date of birth, .age
and .age_group
will be recalculated. And if time passes and the athlete gets older, the code also recalculates .age
and .age_group
each time they're needed.
The .name
Problem
Todd paused to think. There was something else that was bugging him with his code, and he was wondering whether properties could come to the rescue.
Once the club started using his code, Todd realised that having a single .name
data attribute for the full name was a bit restrictive. He wishes he'd used separate data attributes for first and last names. But now, it was too much of a hassle to change all his code.
But we can fix this without making breaking changes to the code. Todd can keep using the full name to create instances of ClubMember
and anywhere else he's using .name
, but also create new attributes for first and last names. And while we're at it, let's keep track of any middle names, too.
First, we can create non-public data attributes for the new name parts and also define them as properties:
These new properties only need getter methods, as you won't need to set their values directly.
Instead, we can set these new non-public data attributes when we set .name
, the attribute that stores the athlete's full name. Let's also convert this data attribute into a property. The .name
property will need both a getter and a setter:
Let's start with the setter method for the new .name
property. First, you assign the value assigned to this property to a non-public data attribute ._name
. Note that this behaviour is slightly different from the one you used for .athlete_id
earlier. We did not replace self.name
with a non-public attribute in .__init__()
. You'll see why soon.
Therefore, you define the non-public attribute ._name
– the one with the leading underscore – directly in the setter method. Next, you assign values to the non-public data attributes ._firstname
, ._middlenames
, and ._lastname
. Note that you use the *
operator to unpack all the middle names in value
, which is the full name, into middlenames
. You then create a string from these middle names in which all middle names are separated by spaces. You'll see this in action soon.
Therefore, when you set the .name
property, you don't just store the full name in ._name
, but you also assign values to the other non-public data attributes that represent the components of the full name.
And this is the reason why you still have self.name = name
in the .__init__()
method. When the .__init__()
method assigns the full name to self.name
, it uses the property's setter method. Therefore, you assign values to ._firstname
, ._middlenames
, and ._lastname
when you initialise the object.
Note that you didn't really need to create ._firstname
, ._middlenames
, and ._lastname
in .__init__()
and assign None
to them. However, this step makes your code more readable as it shows that these data attributes exist directly within the .__init__()
method. However, if you add them to .__init__()
, they need to be defined before you assign a value to .name
!
The .name
getter method is similar to earlier ones you've seen. Since you set the value of .name
in .__init__()
, the non-public data attribute ._name
is guaranteed to exist any time you need to use the .name
getter.
Let's test this. Note that we're adding Michael's middle name, too, when creating the ClubMember
instance:
"Champ" isn't really one of his middle names. But it might as well be!
When you create an instance, you use the athlete's full name. This is Todd's original version. However, this new version creates properties for the first, middle, and last names. Therefore, you can access these name components easily using the appropriate properties.
Then, if you assign a different value to .name
, the .name
setter still deals with updating the other values so you can still access the first, middle, and last names using the respective properties.
You'll find the full version of the code just after the final words in this article.
Final Words
I left Todd to tinker with his code as I went to start my training session. I want to get back in shape quickly.
These updates enabled Todd to improve his code without making any breaking changes to the way the club was already using the code. Properties allow you to create methods and expose them to the user as if they were data attributes. Sometimes, you choose to do this to simplify the interface for your classes. Sometimes, as in Todd's case, properties can save you from having to make breaking changes to your code.
I can't wait to see what else Todd adds to this code next time I meet him at the track.
Photo by Lukas Hartmann: https://www.pexels.com/photo/an-empty-track-and-field-during-nighttime-886696/
Code in this article uses Python 3.13
The code images used in this article are created using Snappify. [Affiliate link]
You can also support this publication by making a one-off contribution of any amount you wish.
Final Version of the Code
For more Python resources, you can also visit Real Python—you may even stumble on one of my own articles or courses there!
Also, are you interested in technical writing? You’d like to make your own writing more narrative, more engaging, more memorable? Have a look at Breaking the Rules.
And you can find out more about me at stephengruppetta.com
Further reading related to this article’s topic:
OOP overview: A Magical Tour Through Object-Oriented Programming in Python • Hogwarts School of Codecraft and Algorithmancy
When Should You Use ‘.__repr__()’ vs ‘.__str__()’ in Python? – Real Python
Appendix: Code Blocks
Code Block #1
# club_membership.py
import datetime
from enum import Enum, IntEnum
class AgeGroup(IntEnum):
U14 = 14
U16 = 16
U18 = 18
U20 = 20
SENIOR = 35
MASTER = -1
class Event(Enum):
T_100M = "100m"
T_200M = "200m"
T_400M = "400m"
T_800M = "800m"
T_1500M = "1500m"
F_LONG_JUMP = "Long Jump"
F_HIGH_JUMP = "High Jump"
F_TRIPLE_JUMP = "Triple Jump"
class ClubMember:
def __init__(self, name, dob, athlete_id=None):
self.name = name
self.dob = dob
today = datetime.date.today()
self.age = (
today.year
- self.dob.year
- (
(today.month, today.day)
< (self.dob.month, self.dob.day)
)
)
self.athlete_id = athlete_id
self.age_group = self.calculate_age_group()
self.events = []
def __str__(self):
return f"{self.name} ({self.age_group.name.title()})"
def __repr__(self):
return (
f"ClubMember({self.name!r}, "
f"{self.dob!r}, {self.athlete_id!r})"
)
@staticmethod
def get_dob_from_string(dob_string, format="%Y-%m-%d"):
return (
datetime.datetime.strptime(
dob_string,
format,
).date()
)
def calculate_age_group(self):
for age_group in AgeGroup:
if self.age < age_group:
return age_group
return AgeGroup.MASTER
def add_events(self, *events_to_add):
if not all(
isinstance(event, Event) for event in events_to_add
):
raise TypeError("Events must be of type 'Event'")
self.events.extend(events_to_add)
def display_events(self):
print(
*(event.value for event in self.events), sep="\n"
)
Code Block #2
# ...
print(AgeGroup.U20.name)
# U20
print(AgeGroup.U20.value)
# 20
Code Block #3
# ...
for item in Event:
print(item)
# Event.T_100M
# Event.T_200M
# Event.T_400M
# Event.T_800M
# Event.T_1500M
# Event.F_LONG_JUMP
# Event.F_HIGH_JUMP
# Event.F_TRIPLE_JUMP
Code Block #4
# ...
print(AgeGroup.U20 + 5)
# 25
Code Block #5
# ...
mj = ClubMember(
"Michael Johnson",
ClubMember.get_dob_from_string("2010-09-13"),
)
print(mj)
# Michael Johnson (U16)
print(repr(mj))
# ClubMember(
# 'Michael Johnson', datetime.date(2010, 9, 13), None
# )
Code Block #6
# ...
mj.add_events(Event.T_400M, Event.T_200M)
mj.display_events()
# 400m
# 200m
Code Block #7
# club_membership.py
# ...
class ClubMember:
def __init__(self, name, dob, athlete_id=None):
# ...
self._athlete_id = athlete_id
self.age_group = self.calculate_age_group()
self.events = []
def __str__(self):
return f"{self.name} ({self.age_group.name.title()})"
def __repr__(self):
return (
f"ClubMember({self.name!r}, "
f"{self.dob!r}, {self.athlete_id!r})"
)
@property
def athlete_id(self):
return self._athlete_id
# ...
Code Block #8
# ...
mj = ClubMember(
"Michael Johnson",
ClubMember.get_dob_from_string("2010-09-13"),
402587023,
)
print(mj.athlete_id)
# 402587023
Code Block #9
# ...
mj = ClubMember(
"Michael Johnson",
ClubMember.get_dob_from_string("2010-09-13"),
402587023,
)
print(mj.athlete_id)
# 402587023
mj.athlete_id = 258762612
Code Block #10
# club_membership.py
# ...
class ClubMember:
# ...
@property
def athlete_id(self):
return self._athlete_id
@athlete_id.setter
def athlete_id(self, value):
if self._athlete_id is not None:
raise ValueError(
"You can't change the value of 'athlete_id'"
)
self._athlete_id = value
# ...
mj = ClubMember(
"Michael Johnson",
ClubMember.get_dob_from_string("2010-09-13"),
402587023,
)
print(mj.athlete_id)
# 402587023
mj.athlete_id = 258762612
Code Block #11
# ...
mj = ClubMember(
"Michael Johnson",
ClubMember.get_dob_from_string("2010-09-13"),
# 402587023,
)
print(mj.athlete_id)
# None
mj.athlete_id = 258762612
print(mj.athlete_id)
# 258762612
Code Block #12
# ...
mj = ClubMember(
"Michael Johnson",
ClubMember.get_dob_from_string("2010-09-13"),
# 402587023,
)
print(mj.athlete_id)
# None
mj.athlete_id = 258762612
print(mj.athlete_id)
# 258762612
mj.athlete_id = 987352942
Code Block #13
# club_membership.py
# ...
class ClubMember:
def __init__(self, name, dob, athlete_id=None):
self.name = name
self.dob = dob
# The lines defining `today` and `self.age`
# are no longer here
self._athlete_id = athlete_id
self.age_group = self.calculate_age_group()
self.events = []
def __str__(self):
return f"{self.name} ({self.age_group.name.title()})"
def __repr__(self):
return (
f"ClubMember({self.name!r}, "
f"{self.dob!r}, {self.athlete_id!r})"
)
@property
def age(self):
today = datetime.date.today()
return (
today.year
- self.dob.year
- (
(today.month, today.day)
< (self.dob.month, self.dob.day)
)
)
# ...
mj = ClubMember(
"Michael Johnson",
ClubMember.get_dob_from_string("2010-09-13"),
)
print(mj.age)
# 14
Code Block #14
# ...
mj.age = 15
Code Block #15
# club_membership.py
# ...
class ClubMember:
def __init__(self, name, dob, athlete_id=None):
self.name = name
self.dob = dob
self._athlete_id = athlete_id
# The line defining `self.age_group` is no longer here
self.events = []
# ...
@property
def age_group(self):
for age_group in AgeGroup:
if self.age < age_group:
return age_group
return AgeGroup.MASTER
# ...
Code Block #16
# ...
mj = ClubMember(
"Michael Johnson",
ClubMember.get_dob_from_string("2010-09-13"),
)
print(mj.age)
# 14
print(mj.age_group.name)
# U16
mj.dob = ClubMember.get_dob_from_string("2007-09-13")
print(mj.age)
# 17
print(mj.age_group.name)
# U18
Code Block #17
# club_membership.py
# ...
class ClubMember:
def __init__(self, name, dob, athlete_id=None):
self._firstname = None
self._lastname = None
self._middlenames = None
self.name = name
self.dob = dob
self._athlete_id = athlete_id
self.events = []
# ...
@property
def firstname(self):
return self._firstname
@property
def middlenames(self):
return self._middlenames
@property
def lastname(self):
return self._lastname
# ...
Code Block #18
# club_membership.py
# ...
class ClubMember:
def __init__(self, name, dob, athlete_id=None):
self._firstname = None
self._lastname = None
self._middlenames = None
self.name = name
self.dob = dob
self._athlete_id = athlete_id
self.events = []
# ...
@property
def firstname(self):
return self._firstname
@property
def middlenames(self):
return self._middlenames
@property
def lastname(self):
return self._lastname
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
self._firstname, *middlenames, self._lastname = value.split()
self._middlenames = " ".join(middlenames)
# ...
Code Block #19
# ...
mj = ClubMember(
"Michael Duane Johnson",
ClubMember.get_dob_from_string("2010-09-13"),
)
print(mj.firstname)
# Michael
print(mj.middlenames)
# Duane
print(mj.lastname)
# Johnson
mj.name = "Mike Duane Champ Johnson"
print(mj.firstname)
# Mike
print(mj.middlenames)
# Duane Champ
print(mj.lastname)
# Johnson
Code Block #20
# club_membership.py
import datetime
from enum import Enum, IntEnum
class AgeGroup(IntEnum):
U14 = 14
U16 = 16
U18 = 18
U20 = 20
SENIOR = 35
MASTER = -1
class Event(Enum):
T_100M = "100m"
T_200M = "200m"
T_400M = "400m"
T_800M = "800m"
T_1500M = "1500m"
F_LONG_JUMP = "Long Jump"
F_HIGH_JUMP = "High Jump"
F_TRIPLE_JUMP = "Triple Jump"
class ClubMember:
def __init__(self, name, dob, athlete_id=None):
self._firstname = None
self._lastname = None
self._middlenames = None
self.name = name
self.dob = dob
self._athlete_id = athlete_id
self.events = []
def __str__(self):
return f"{self.name} ({self.age_group.name.title()})"
def __repr__(self):
return (
f"ClubMember({self.name!r}, "
f"{self.dob!r}, {self.athlete_id!r})"
)
@property
def firstname(self):
return self._firstname
@property
def middlenames(self):
return self._middlenames
@property
def lastname(self):
return self._lastname
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
self._firstname, *middlenames, self._lastname = value.split()
self._middlenames = " ".join(middlenames)
@property
def age(self):
today = datetime.date.today()
return (
today.year
- self.dob.year
- (
(today.month, today.day)
< (self.dob.month, self.dob.day)
)
)
@property
def age_group(self):
for age_group in AgeGroup:
if self.age < age_group:
return age_group
return AgeGroup.MASTER
@property
def athlete_id(self):
return self._athlete_id
@athlete_id.setter
def athlete_id(self, value):
if self._athlete_id is not None:
raise ValueError(
"You can't change the value of 'athlete_id'"
)
self._athlete_id = value
@staticmethod
def get_dob_from_string(dob_string, format="%Y-%m-%d"):
return (
datetime.datetime.strptime(
dob_string,
format,
).date()
)
def add_events(self, *events_to_add):
if not all(
isinstance(event, Event) for event in events_to_add
):
raise TypeError("Events must be of type 'Event'")
self.events.extend(events_to_add)
def display_events(self):
print(
*(event.value for event in self.events), sep="\n"
)
For more Python resources, you can also visit Real Python—you may even stumble on one of my own articles or courses there!
Also, are you interested in technical writing? You’d like to make your own writing more narrative, more engaging, more memorable? Have a look at Breaking the Rules.
And you can find out more about me at stephengruppetta.com