Hark, 'tis of Data Validation and Classes, forsooth,

Skip to content

This doth be a machine-wrought text which may contain errors!

In Python (and ‘tis true of most other tongues), ‘tis possible to create one’s own objects, with values, rules, and functions peculiar unto them. These are called Classes (in the English fashion). We employ Classes to gather related data and functions into a single unit, for example, an Ordre Class which doth hold data such as ordre_id, kunde_navn, produkter, and functions like legg_til_produkt(), beregn_total(), and the like.

Dictionary (JSON) vs Klasser (Objekter)

JSON (JavaScript Object Notation) doth be a form for to store and convey data, independent of the tongue of programming, whilst a class is a structure within a specific tongue of programming.

When we would send data o’er the network, or store it within a file, ‘tis oft JSON we employ (or tables within databases).

When we would work with structured data in code, ‘tis classes we do use.

In this module, we shall observe how we may employ classes to validate data.

The easiest course is to make use of @dataclass from the dataclasses library. This doth allow us to eschew the writing of much boilerplate code for the creation of a class. (Such as, for example, the built-in __init__ and __repr__ (representation) functions).

An example of a class without employing 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 # Forsooth, this doth print the car's details.

British python devs be like thats a constructor, __init__?

An Example with the dataclass Decorator, which doth achieve 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)  # Utdata: Bil(merke='Toyota', modell='Corolla', år=2020)  # Output: Car(make='Toyota', model='Corolla', year=2020)

Easy Task the First - To Forge a Class

Forge ye a class hight Person. This class shall possess these qualities (attributes):

  • name: The name of the personage
  • eye_color: The hue of the person’s eyes
  • phone_number: The person’s number for telephony
  • email: The person’s address for electronic post

Instantiate (take into use) an object of the class Person with values meet for all qualities, as in the example hereunder.

@dataclass
class Person:
    ... # Thy code doth reside 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 doth lie a possible resolution:

from dataclasses import dataclass

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

Medium Task the Second – Validation within the Class

In the example yclept above, we have not added any validation, good sir. ‘Tis to say, we may fashion a Person with values most unsound, such as:

@dataclass
class Person:
    ... # Thy code doth reside 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 doth (perchance) present a troublesome matter, and may well accrue technical debt in times to come. Thankfully, there exist simple ways to add validation unto classes.

We shall begin by beholding email validation. There are libraries built within Python which may aid us in this endeavour, but forasmuch as we do this to learn, we shall craft our own simple validation, by creating a new class for “Email”, and examining a __post_init__ function (for dataclass only).

Merk

We may eke do this within the very Person class, yet ‘tis oft better to fashion separate classes for such things as may be reused.

An exemplification of a post_init function
from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        print(f"Validating email: {self.address}")
        # Thine own code doth reside here

For a simple email validation, we may perchance check that the email doth contain both @ and . characters. Peradventure thou mayest also check that the email doth match a regex pattern. (More advanced, yet seek ye upon the web, I pray!)

Løsning: Kode for enkel e-post validering

Here doth lie a possible solution, where we employ exceptions to “crash” should the email prove invalid, thus halting the program forthwith and yielding 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 koden 
test = Email("hei@example.com")  # Gyldig
try:
    test = Email("heiexample.com")
except ValueError as e:
    print(e) # Ugyldig, mangler @

Medium Task the Third – Telephone Number Validation

Create, in like manner as thou didst for electronic missives, a class, but now for telephone numbers.

Challenge with telephone validation!

Prithee, canst thou mend the validation for telephone numbers to accept both letters (str) and numbers (int)? For example, 12345678 and "12345678" should both be valid.

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

Medium Task the Fourth - Employ Validation within the Person Class

Now that we have wrought validation for email and telephone number, we may employ these within the Person class.

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: PhoneNumber  # Prithee, employ the PhoneNumber class
    email: Email               # Pray, make use of the Email class

A new challenge doth arise!

Now that we have altered the Person class to employ the PhoneNumber and Email classes, we must also amend how we instantiate (create) a Person. We must now first fashion a PhoneNumber and an Email object, ere we may create a Person.

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number=PhoneNumber("12345678"),  # Hark, a change doth here reside!
                   email=Email("bob_kaare@example.com"))  # Prithee, observe a change doth here abide!
print(bob_kaare)

# Attend! A change must needs be wrought in how we extract the values also
print(bob_kaare.email.address)
print(bob_kaare.phone_number.number)  # .country_code(?)

Hard Task 5 - Properties in Classes (Optional)

When we do employ objects to represent values such as e-mail and telephone number, ‘tis needful we specify the sub-value (for example, address for e-mail and number for telephone number) each time we seek to extract the value. This may become somewhat cumbersome in the long run. Thankfully, there doth exist a solution to this, by the use of the @property decorator within a class, which alloweth us to extract the value directly from the object, without needing to specify the sub-value.

This doth, however, present yet another challenge, and ‘tis that we require the __init__ function within the Person class. This is because we cannot employ the same name for both a property and an attribute within a dataclass.

from dataclasses import dataclass

@dataclass
class EksempelVerdi:
    attributt: str

@dataclass
class Person:
    name: str
    _verdi: EksempelVerdi  # An inward variable (doth begin with _ to signify 'tis "private")

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

    @property
    def verdi(self):
        return self._verdi.attributt  # Doth fetch the under-value directly

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

Merk

Properties be singular in that they require not parameters, nor need parentheses to be run. In the example, we do fetch forth person.verdi without parentheses (not person.verdi()), albeit ‘tis a function technically.

Alternativt eksempel som aksepterer både str og 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 må være av type str eller EksempelVerdi")

    @property
    def verdi(self) -> str:
        return self._verdi.attributt  # Henter ut underverdien direkte

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

Hard Task 6 - Properties with Logic (Optional)

Update Person by adding a new attribute hight birthday. This shall be of the type datetime.date (from the datetime library).

Thereafter, thou shalt create the following properties:

  • Create a property age which doth calculate the age of the person based upon birthday and the current date.
  • Create a property is_adult which returneth True if the person is 18 years or older, else False.

Løsning: Alder og voksen som properties

Here doth lie a possible resolution:

from dataclasses import dataclass
from datetime import date

@dataclass
class Person:
    name: str
    birthday: date

    @property
    def age(self) -> int:
        """Doth calculate the age based upon the birthdate and the current date"""
        today = date.today()
        age = today.year - self.birthday.year
        # Adjusteth downward by one, should the person not yet have had their birthday this year
        if (today.month, today.day) < (self.birthday.month, self.birthday.day):
            age -= 1
        return age

    @property
    def is_adult(self) -> bool:
        """Returneth True if the person be eighteen 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 the current date be after the 15th of May, 2023
print(person.is_adult)  # True

Yet another challenge!

Canst thou achieve instantiation of Person to accept both datetime.date and a text in the fashion of "DD-MM-YYYY" for birthday? (Hint: employ datetime.strptime to convert the string to a datetime.date)