This here’s a machine-translated text that might contain some errors!
Data Validation and Classes
In Python (this also applies to 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 bundle related data and functions into a single unit, for example an Order class that has data like order_id, customer_name, products, and functions such as add_product(), calculate_total(), etc.
Dictionaries (JSON) ‘n Classes (Objects)
JSON (JavaScript Object Notation) is a way to store an’ pass along data no matter what language you’re wranglin’, while a class is just a structure in one specific programming tongue.
When we need to shoot data across the network, or stash it away on disk, we often use JSON (or tables if we’re dealin’ with databases).
But when we’re workin’ with structured data right here in code, that’s where classes come into play.
In this module, we’ll look at how we can use classes to validate data.
Video: How classes gather data and rules
The video explains what a class is, and how classes can be used to structure and validate data.
Easiest way’s usin’ the @dataclass from the dataclasses library. This lets you skip writin’ all that boilerplate code just to make a class. (Like them built-in __init__ an’ __repr__ (representation) functions.)
Example of a class without usin’ 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
Here’s an example with a dataclass decorator, which gets ya that same result as before (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 - Build a Class
Build a class named Person. This class needs to have these properties (attributes):
name: The person’s nameeye_color: The person’s eye colorphone_number: The person’s phone numberemail: The person’s email address
Instantiate (put into use) an object of the Person class with valid values for all properties, just like 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 fer Person
Here’s 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. That means we can create a Person with invalid values, like this:
@dataclass
class Person:
... # Yer 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')
That’s a bit troublesome, partner, and could sure enough saddle us with technical debt down the road. Lucky stars though, there ain’t no hard way to put validation into classes.
Let’s start off by lookin’ at email validation. There be built-in libraries in Python that can lend a hand with this here matter, but since we’re doin’ it all so as to learn, we’ll whip up our own simple validation, by craftin’ a new class fer “Email” and checkin’ out a __post_init__ function (just fer dataclasses).
Markin’ it up
We can also handle this right in that Person class, but it’s often better to whip up our own classes for things we might use again down the road.
Example of an _postinit function
from dataclasses import dataclass
@dataclass
class Email:
address: str
def __post_init__(self):
print(f"Validatin' that email: {self.address}")
# Yer code goes here
Folks, for a simple email check, we can look see if it got both them @ an’ . signs in there. Or maybe you could make sure it matches up with one o’ those fancy regex patterns. (That’s a mite more advanced stuff, but don’t be shy ‘bout huntin’ around on that ol’ internet!)
Solution: Code for simple email validation
Here’s a possible solution, we use exceptions to “crash” if the email is invalid, this will stop the program immediately and give 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 the email address: {self.address}")
if "." not in self.address.split("@")[1]:
raise ValueError(f"Missing . in the domain of the email address: {self.address}")
if " " in self.address:
raise ValueError(f"The email address cannot contain spaces: {self.address}")
# Test the code
test = Email("hey@example.com") # Valid
try:
test = Email("heyexample.com")
except ValueError as e:
print(e) # Invalid, missing @
Task 3 - Phone Number Validation
Create a class similar to the one ya made for email addresses, but now for phone numbers.
Phone Verification Trouble!
Can you fix phone number validation to accept both letters (strings) and numbers (ints)? For example, 12345678 and "12345678" should both be valid.
Also try adding country codes as an attribute (a sub-value within the class). For instance, 47 for Norway, 46 for Sweden
Task 4 - Use validation in the Person class
Now that we’ve got our email and phone number validations all set up, let’s put ‘em to work 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
A new challenge arises!
Now that we’ve modified the Person class to use the PhoneNumber and Email classes, we gotta change how we instantiate (create) a Person. We need to first whip up a PhoneNumber and an Email object before we can make 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 fetch 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 hold values like email and phone numbers, we’re stuck havin’ to specify the sub-value (like address fer emails an’ number fer phones) every single time we want ta pull out that value. That can get real cumbersome over a stretch o’ code. Lucky fer us, there’s a fix fer this one — just slap on tha @property decorator inside a class, an’ y’all can grab that value straight from the object without havin’ ta name which part ya mean.
That said, it brings up another hitch: we need tha __init__ function inside tha Person class. The reason is simple enough — ya can’t use th same name fer both a property an’ an attribute inside a dataclass.
from dataclasses import dataclass
@dataclass
class ExampleValue:
attribute: str
@dataclass
class Person:
name: str
_value: ExampleValue # Internal variable (starts with _ to indicate it's 'private')
def __init__(self, name: str, value: ExampleValue):
self.name = name
self._value = value
@property
def value(self):
return self._value.attribute # Retrieves the sub-value directly
# Test code
person = Person(name="Alice", value=ExampleValue("Some text"))
print(person.value) # Output: Some text
Note
Properties are unique in that they don’t need parameters an’ ain’t got to use no parentheses fer runnin’. In this example, we’re pullin’ out person.value without any parens (not person.value()) even though it’s technically a function.
Alternative example that accepts both str and ExampleValue
@dataclass
class Person:
name: str
_value: str
def __init__(self, name: str, value: str | ExampleValue):
self.name = name
if isinstance(value, ExampleValue):
self._value = value
elif isinstance(value, str):
self._value = ExampleValue(value)
else:
raise TypeError("value must be of type str or ExampleValue")
@property
def value(self) -> str:
return self._value.attribute # Fetches 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 addin’ a new attribute named birthday. This one’s gotta be of type datetime.date (from the datetime library).
Then you’ll need to whip up these here properties:
- Cook up a property
agethat figures out the person’s age based onbirthdayan’ today’s date. - Build a property
is_adultthat spits backTrueif the feller is 18 years old or older, else it gives yaFalse.
Solution: Age and adult as properties
Here’s 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 hasn't had their birthday this year yet
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 or older"""
return self.age >= 18
# Test code
person = Person(name="Alice", birthday=date(2005, 5, 15))
print(person.age) # E.g. 18 if today's date is after May 15, 2023
print(person.is_adult) # True
Another challenge, partner!
Can you get the instantiation of Person to accept both a datetime.date and a string in the format "DD-MM-YYYY" for birthday? (Tip: use datetime.strptime to convert that string into a datetime.date)
