Hopp til innhold

Data validation and classes,

Skip to content

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

British python devs be like thats a constructor, __init__?

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)

Easy Task 1 - Build a Class

Build a class named Person. This class needs to have these properties (attributes):

  • name: The person’s name
  • eye_color: The person’s eye color
  • phone_number: The person’s phone number
  • email: 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

Medium 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 @

Medium 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

Medium 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(?)

Hard 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

Hard 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 age that figures out the person’s age based on birthday an’ today’s date.
  • Build a property is_adult that spits back True if the feller is 18 years old or older, else it gives ya False.
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)