This is a machine-translated text that may contain errors!
Data validation and classes
In Python (as is also possible in most other languages), it is possible to create your own objects with their own values, rules, and functions. These are called classes. We use classes to group related data and functions into a single unit, e.g., an Order class that has data such as order_id, customer_name, products and functions like add_product(), calculate_total(), etc.
Dictionaries (JSON) vs Classes (Objects)
JSON (JavaScript Object Notation) is a format for storing and transferring data independent of programming language, while a class is a structure in a specific programming language.
When we need to send data over the network, or store it in a file, we often use JSON (or tables in databases).
When working with structured data in code, we use classes.
In this module, we will look at how we can use classes to validate data.
Video: How classes collect data and rules
The video explains what a class is, and how classes can be used to structure and validate data.
The easiest way is to use @dataclass from the dataclasses library. This allows us to avoid writing much boilerplate code to create a class. (Such as the built-in __init__ and __repr__ (representation) functions).
Example of a class without using the dataclass decorator:
class Car:
def __init__(self, make: str, model: str, year: int):
self.make = make
self.model = model
self.year = year
def __repr__(self):
return f"{self.year} {self.make} {self.model}"
my_car = Car("Toyota", "Corolla", 2020)
print(my_car) # Output: 2020 Toyota Corolla
Example using the dataclass decorator, which achieves the same as above (but with less code):
from dataclasses import dataclass
@dataclass
class Car:
make: str
model: str
year: int
my_car = Car("Toyota", "Corolla", 2020)
print(my_car) # Output: Car(make='Toyota', model='Corolla', year=2020)
Task 1 - Create a Class
Create a class named Person. This class should have the following properties (attributes):
name: The name of the personeye_color: The eye color of the personphone_number: The phone number of the personemail: The email address of the person
Instantiate (use) an object of the Person class with valid values for all properties as in the example below.
@dataclass
class Person:
... # Your code here
bob_kaare = Person(name="Bob Kåre",
eye_color="blue",
phone_number="12345678",
email="bob_kaare@example.com")
print(bob_kaare)
Solution: A dataclass for Person
Here is one possible solution:
from dataclasses import dataclass
@dataclass
class Person:
name: str
eye_color: str
phone_number: str
email: str
Task 2 - Validation in class
In the example above we have not added any validation. This means that we can create a Person with invalid values, such as:
@dataclass
class Person:
... # Your code here
invalid_person = Person(name="",
eye_color="yes",
phone_number="12345",
email="not-an-email")
print(invalid_person)
# Output: Person(name='', eye_color='yes', phone_number='12345', email='not-an-email')
This is (potentially) problematic and can easily lead to technical debt in the future. Fortunately, there are simple ways to add validation to classes.
We will start by looking at email validation. There are built-in libraries in Python that can help us with this, but because we are doing this to learn, we will create our own simple validation by creating a new class for “Email” and investigating a __post_init__ function (only for dataclass).
Note
We can also do this in the Person class itself, but it’s often better to create separate classes for things that can be reused.
Example of an __init__ function
from dataclasses import dataclass
@dataclass
class Email:
address: str
def __post_init__(self):
print(f"Validating email: {self.address}")
# Your code here
For a simple email validation, we can for example check that the email contains both @ and . characters. Optionally, you can check that the email matches a regex pattern. (More advanced, but feel free to search online!)
Solution: Code for simple email validation
Here is a possible solution; we use exceptions to “crash” if the email is invalid. This will stop the program immediately and provide an error message.
from dataclasses import dataclass
@dataclass
class Email:
address: str
def __post_init__(self):
if "@" not in self.address:
raise ValueError(f"@ missing in email address: {self.address}")
if "." not in self.address.split("@")[1]:
raise ValueError(f". missing in domain of email address: {self.address}")
if " " in self.address:
raise ValueError(f"Email address cannot contain spaces: {self.address}")
# Test code
test = Email("hei@example.com") # Valid
try:
test = Email("heiexample.com")
except ValueError as e:
print(e) # Invalid, missing @
Task 3 - Phone number validation
Create a similar class to what you made for emails, but now for phone numbers
Phone validation challenge!
Can you fix phone number validation to accept both letters (str) and numbers (int)? E.g., 12345678 and "12345678" should both be valid.
Also try adding country codes as an attribute (subvalue to the class). E.g., 47 for Norway, 46 for Sweden
Task 4 - Use validation in the Person class
Now that we have created validation for email and phone number, we can use these in the Person class.
@dataclass
class Person:
name: str
eye_color: str
phone_number: PhoneNumber # Use the PhoneNumber class
email: Email # Use the Email class
New challenge arises!
Now that we have changed the Person class to use the PhoneNumber and Email classes, we must also change how we instantiate (create) a Person. We must now first create a PhoneNumber and an Email object, before we can create a Person.
bob_kaare = Person(name="Bob Kåre",
eye_color="blue",
phone_number=PhoneNumber("12345678"), # Note the change here
email=Email("bob_kaare@example.com")) # Note the change here
print(bob_kaare)
# Note: a change must be made in the way we retrieve the values as well
print(bob_kaare.email.address)
print(bob_kaare.phone_number.number) # .country_code(?)
Task 5 - Properties in Classes (Optional)
When we use objects to represent values such as email and phone number, we are required to specify the sub-value (e.g., address for email and number for phone number) every time we retrieve the value. This can become cumbersome over time. Fortunately, there is a solution: using the @property decorator in a class allows us to retrieve the value directly from the object without having to specify the sub-value.
However, this introduces another challenge: we need an __init__ function in the Person class. This is because we cannot use the same name for both a property and an attribute in a dataclass.
from dataclasses import dataclass
@dataclass
class EksempelVerdi:
attributt: str
@dataclass
class Person:
name: str
_verdi: EksempelVerdi # Internal variable (starts with _ to indicate it is "private")
def __init__(self, name: str, verdi: EksempelVerdi):
self.name = name
self._verdi = verdi
@property
def verdi(self):
return self._verdi.attributt # Retrieves the sub-value directly
# Test the code
person = Person(name="Alice", verdi=EksempelVerdi("Noe tekst"))
print(person.verdi) # Output: Noe tekst
Note
Properties are unique in that they don’t require parameters and don’t need parentheses to be executed. In the example, we retrieve person.value without parentheses (not person.value()) even though it technically is a function.
Alternative example that accepts both str and EksempelVerdi
@dataclass
class Person:
name: str
_value: str
def __init__(self, name: str, value: str | EksempelVerdi):
self.name = name
if isinstance(value, EksempelVerdi):
self._value = value
elif isinstance(value, str):
self._value = EksempelVerdi(value)
else:
raise TypeError("value must be of type str or EksempelVerdi")
@property
def value(self) -> str:
return self._value.attribute # Retrieves the sub-value directly
# Test code
person = Person(name="Alice", value="Some text")
print(person.value) # Output: Some text
Task 6 - Properties with logic (Optional)
Update Person by adding a new attribute named birthday. This should be of type datetime.date (from the datetime library).
Next, you should create the following properties:
- Create a property
agethat calculates the person’s age based onbirthdayand today’s date. - Create a property
is_adultthat returnsTrueif the person is 18 years old or older, otherwiseFalse.
Solution: Age and adult as properties
Here is a possible solution:
from dataclasses import dataclass
from datetime import date
@dataclass
class Person:
name: str
birthday: date
@property
def age(self) -> int:
"""Calculates the age based on birthdate and current date"""
today = date.today()
age = today.year - self.birthday.year
# Adjust down by 1 if the person has not had their birthday yet this year
if (today.month, today.day) < (self.birthday.month, self.birthday.day):
age -= 1
return age
@property
def is_adult(self) -> bool:
"""Returns True if the person is 18 years old or older"""
return self.age >= 18
# Test code
person = Person(name="Alice", birthday=date(2005, 5, 15))
print(person.age) # E.g. 18 if the current date is after May 15, 2023
print(person.is_adult) # True
Yet another challenge!
Can you get the instantiation of Person to accept both datetime.date and a text in the format "DD-MM-YYYY" for birthday? (Hint: use datetime.strptime to convert the string to a datetime.date)
