Avast! Data be checkin' and classes, aye!

Skip to content

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

In Python (and this be true for most other tongues), ‘tis possible to create yer own objects, with their own values, rules, and functions. These be called classes (classes in the English tongue). We use classes to gather related data and functions into a single unit, fer example, an Ordre (Order) class that has data such as ordre_id, kunde_navn (customer name), produkter (products) and functions like legg_til_produkt() (add product), beregn_total() (calculate total), and so on.

Dictionary (JSON) vs Klasser (Objekter)

JSON (JavaScript Object Notation) be a format fer storin’ an’ transferrin’ data, independent o’ any programmin’ tongue, while a class be a structure within a specific programmin’ tongue.

When we be needin’ ter send data across the network, or store it in a file, we often use JSON (or tables in databases).

When we be needin’ ter work with structured data in 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)  # Avast! 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)

Løsning: En 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).

Merk

We can also do this within the Person class itself, but ‘tis often better to craft separate 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!)

Løsning: Kode for enkel e-post validering

Here be a possible solution, we be usin’ exceptions to “crash” if the email be invalid, this’ll stop the program right quick, 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"Mangler @ i epostadressen: {self.address}")
        if "." not in self.address.split("@")[1]:
            raise ValueError(f"Mangler . i domenet til epostadressen: {self.address}")
        if " " in self.address:
            raise ValueError(f"Epostadressen kan ikke inneholde mellomrom: {self.address}")

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

Medium Task 3 - Telephone Number Validation

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

Challenge with telephone validation!

Can ye fix the validation for telephone numbers to accept both letters (str) an’ numbers (int)? For example, 12345678 an’ "12345678" should both be valid.

Try also to add country codes as an attribute (sub-value to the class). For example, 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?

A 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"),  # 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  # A secret stash, aye! (begynner med _ for å indikere at den er "privat")

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

    @property
    def verdi(self):
        return self._verdi.attributt  # Fetchin' the treasure, savvy?

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

Merk

Properties be unique in that they need no parameters, nor do they require parentheses to be run. In the example, we fetch person.verdi without parentheses (not person.verdi()), even though it be technically a function.

Alternatively, an 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  # Fetch the underlying value 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:
        """Calculates the age based on the date of birth and today's date"""
        today = date.today()
        age = today.year - self.birthday.year
        # Adjust down by 1 if the person hasn't 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 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's date is after May 15, 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)