Data checkin' and classes,

Skip to content

Avast ye, this be a machine-translated text an’ may contain errors, aye!

In Python (be true for most other tongues as well), ye can create yer own objects, with their own values, rules, and functions. This be called classes. We use classes to gather related booty and functions into one unit, like an Ordre class that holds data such as ordre_id, kunde_navn, produkter and functions like legg_til_produkt(), beregn_total(), and so on.

Dictionary (JSON) vs Classes (Objects)

JSON (JavaScript Object Notation) be a format for storin’ and passin’ booty across different tongues, while a class be a structure within a specific code-script.

When we be sendin’ loot over the network, or hidin’ it away in a file, we often use JSON (or tables in the databases).

When we be workin’ with structured booty in our code, we use classes.

In this module, we shall be lookin’ at how we can use classes to validate data.

The easiest way be to use @dataclass from the dataclasses library. This allows us to avoid writin’ a lot o’ boilerplate code to create a class. (Such as the built-in __init__ and __repr__ (representation) functions).

An example o’ 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__?

An example usin’ 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)  # Aye, 'tis the output, matey: Car(make='Toyota', model='Corolla', year=2020)

Easy Task 1 - Craft a Class

Craft a class be called Person. This class shall have the followin’ properties (attributes):

  • name: The name o’ the person
  • eye_color: The eye color o’ the person
  • phone_number: The phone number o’ the person
  • email: The email address o’ the person

Instantiate (bring into use) an object o’ the class Person with valid values for all properties, as in the example below.

@dataclass
class Person:
    ... # Yer code be 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 be a possible solution:

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: str
    email: str

Medium Task 2 - Validatin’ in the Class

In the example above, we be havin’ not added any validatin’. That means we can create a Person with ill-gotten values, such as:

@dataclass
class Person:
    ... # Yer code be 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')

Aye, this be (potentially) troublesome, an’ may well lead to technical debt in the future, savvy? Thankfully, there be simple ways to add validation to yer classes.

We shall begin by lookin’ at e-mail validation. There be built-in libraries in Python that can aid us with this, but seein’ as we be doin’ this to learn, we shall craft our own simple validation, by creatin’ a new class for “Email”, an’ examin’ a __post_init__ function (fer dataclasses only).

Mark

We can also do this within the Person class itself, but it be often better to forge yer own classes for things that can be reused.

An example o’ a post_init function
from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        print(f"Validatin' the email: {self.address}")
        # Yer code here

For a simple email validation, we can, fer example, check that the email contains both @ and . characters. Aye, or ye can check that the email matches a regex pattern. (More advanced, but search the web, ye scurvy dog!)

Solution: Code for simple email validation

Here be a possible way to fix it, we be usin’ exceptions to “crash” the ship if the email be invalid, this will stop the program dead in its tracks and give ye 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 any spaces: {self.address}")

# Test the code 
test = Email("hei@example.com")  # Valid
try:
    test = Email("heiexample.com")
except ValueError as e:
    print(e) # Invalid, missin' the @

Medium Task 3 - Telephone Number Validation

Create a class similar to the one ye made for email addresses, but now for telephone numbers.

Trouble with the scallywag’s phone validation!

Can ye fix the validation for them phone numbers to accept both letters (str) and digits (int)? For example, 12345678 and "12345678" must both be valid booty.

Try also to add country codes as an attribute (sub-value to the class). For instance, 47 for Norway, 46 for Sweden

Medium Task 4 - Use Validation in the Person Class

Now that we’ve crafted validation for email and telephone number, we can be usin’ ‘em in the Person class, aye.

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: PhoneNumber  # Use the PhoneNumber class, aye!
    email: Email               # Use the Email class, savvy?

Ny utfordring oppstår!

Now that we’ve changed the Person class to use PhoneNumber and Email classes, we must also change how we instantiate (create) a Person. We must now first craft a PhoneNumber and an Email object, before we can spawn a Person.

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number=PhoneNumber("12345678"),  # Avast, a change be made here!
                   email=Email("bob_kaare@example.com"))  # Shiver me timbers, a change be made here!
print(bob_kaare)

# Aye, a change must be made in how we be gettin' 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 be usin’ objects to represent values like e-mail and phone numbers, we be needin’ to specify the sub-value (e.g. address for e-mail and number for phone number) each time we be wantin’ to retrieve the value. This can be a bit cumbersome in the long run. Luckily, there be a solution to this, by usin’ the @property decorator in a class, which allows us to retrieve the value directly from the object, without havin’ to specify the sub-value.

This does, however, present yet another challenge, and that be that we be needin’ the __init__ function in the Person class. This be 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 booty (starts with _ to show it be "private")

    def __init__(self, name: str, verdi: EksempelVerdi):
        self.name = name
        self._verdi = verdi

    @property
    def verdi(self):
        return self._verdi.attributt  # Reaches for the sub-treasure directly

# Test the code
person = Person(name="Alice", verdi=EksempelVerdi("Noe tekst"))
print(person.verdi)  # Output: Noe tekst

Merk

Properties be all unique ‘cause they don’t need no parameters, and don’t require no parentheses to run. In this here example, we be fetchin’ person.verdi without them parentheses (not person.verdi()), even though it be a function by the laws o’ the sea.

Another booty-ful example that accepts both str and EksempelVerdi
@dataclass
class Person:
    name: str
    _verdi: str

    def __init__(self, name: str, verdi: str | EksempelVerdi):
        self.name = name
        if isinstance(verdi, EksempelVerdi):
            self._verdi = verdi
        elif isinstance(verdi, str):
            self._verdi = EksempelVerdi(verdi)
        else:
            raise TypeError("verdi must be of type str or EksempelVerdi")

    @property
    def verdi(self) -> str:
        return self._verdi.attributt  # Reaching for the treasure directly

# Test the code
person = Person(name="Alice", verdi="Some text")
print(person.verdi)  # Output: Some text

Hard Task 6 - Properties with Logic (Optional)

Update Person by addin’ a new attribute called birthday. This be o’ the type datetime.date (from the datetime library).

Then ye shall create the followin’ properties:

  • Create a property age that calculates the person’s age based on birthday and the current date.
  • Create a property is_adult that returns True if the person be 18 years or older, else False.

Solution: Age and adult as properties

Here be a possible solution:

from dataclasses import dataclass
from datetime import date

@dataclass
class Person:
    name: str
    birthday: date

    @property
    def age(self) -> int:
        """Calculatin' the age based on birth date and today's date"""
        today = date.today()
        age = today.year - self.birthday.year
        # Drop it by 1 if the scallywag 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 swashbuckler be 18 years or older"""
        return self.age >= 18

# Test the code
person = Person(name="Alice", birthday=date(2005, 5, 15))
print(person.age)       # E.g., 18 if today be after May 15th, 2023
print(person.is_adult)  # True

Another challenge, aye!

Can ye manage to get instantiation o’ 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)