Data validation an' classes,

Skip to content

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

Data Validation an’ Classes

In Python (an’ most other tongues), ye can forge yer own objects, with yer own values, rules, an’ functions. This is called classes (in English tongue). We use classes to bundle related data an’ functions into one piece, like an Order class that has data like order_id, customer_name, products, an’ functions like add_product(), calculate_total(), an’ such.

The Logbook (JSON) be at odds with Ye’ Ship’s Rigs (Objects)

JSON (JavaScript Object Nomenclature) be a method fer hoardin’ an’ passin’ cargo ‘cross languages, whilst a class be but one rig on a single ship.

When we need to haul supplies across the sea lanes, or bury them in a cache o’ files, we often use JSON (or tables in our treasure maps).

When we be workin’ with structured plunder aboard, then we use classes.

In this module, we shall look at how ye can use classes to validate yer data.

Vidiyo: How Classes Gather Data an’ Rules

The vidiyo explains what a class be, an’ how classes can be used to structure an’ validate the booty of datums.

The easiest way be to use @dataclass from the dataclasses library. This lets us skip writin’ much boilerplate code fer creatin’ a class. (Like the built-in __init__ an’ __repr__ (representation) functions.)

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 scallywags be like that's a constructor, __init__?

A sample with the dataclass decorator, which achieves the same as afore mentioned (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)  # The treasure ye'll spy: Car(make='Toyota', model='Corolla', year=2020)

Easy Task 1 - Forge a Class

Forge ye a class named Person. This beast o’ code shall possess the followin’ attributes:

  • name: The name o’ the scallywag
  • eye_color: The hue o’ the scallywag’s eyes
  • phone_number: The number fer hollerin’ at the scallywag
  • email: The email address to send parrots t’ the scallywag

Instantiate (bring into use) an object from the Person class with valid values fer all the properties, just like in the example below.

@dataclass
class Person:
    ... # Yer code here arr!

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 be one possible solution:

from dataclasses import dataclass

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

Medium Task 2 - Verification aboard ship

In the example above, we have not added any verification. That means we can forge a Person with improper values, like so:

@dataclass
class Person:
    ... # Yer scurvy code here

invalid_person = Person(name="",
                        eye_color="yes",
                        phone_number="12345",
                        email="not-an-email")
print(invalid_person)
# The booty ye'll see: Person(name='', eye_color='yes', phone_number='12345', email='not-an-email')

Ahoy! This be a potential curse upon our ship’s hold, and may soon breed technical debt in the days to come. Blessed be there be simple ways to rig validation into our classes.

We shall commence by spyin’ on email verification. There be built-in libraries aboard Python that can lend us a hand with this task, but since we do it all fer learnin’, we’ll forge our own rudimentary check. We’ll build a fresh class fer “Email” an’ poke at the __post_init__ function (dataclasses only, ye scurvy dogs!).

Mark

We can also do this in the very Person class, but it be often better to forge yer own classes fer things that can be reused.

An example o’ a _postinit function
from dataclasses import dataclass

@dataclass
class Email:
    address: str

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

Fer a simple email check, we can spy if it got both an @ sign and a dot mark in her holdin’s. Or ye might match her against them fancy pattern runes. (More advanced sea-business, but do search the wide web for more treasure, matey!)

Solution: Code fer simple email validation

Here be a possible solution, we use exceptions to “crash” should the email be invalid, this will stop the program at once 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"@ is missing in the email address: {self.address}")
        if "." not in self.address.split("@")[1]:
            raise ValueError(f". is 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("hei@example.com")  # Valid
try:
    test = Email("heiexample.com")
except ValueError as e:
    print(e) # Invalid, missing @

Medium Task 3 - Telephone Number Validation

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

A scurvy telephone verification challenge be blowin’ in!

Can ye fix the validation fer phone numbers to accept both letters (strings) an’ digits (ints), matey? Arr, 12345678 an’ "12345678" should both be valid booty.

Also try addin’ country codes as a treasure attribute (a sub-value t’ the class), if yer askin’. Like 47 fer Norway, or 46 fer Sweden

Medium Task 4 - Use validation in the Person class

Now that we’ve forged our email an’ phone number validations, we can put ‘em to use 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 becometh us!

Now that we have altered the Person class to employ PhoneNumber an’ Email classes, we musk also alter how we instantiate (forge) a Person. We musk now first forge a PhoneNumber an’ an Email object afore we can forge a Person.

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number=PhoneNumber("12345678"),  # Mark the change here, matey
                   email=Email("bob_kaare@example.com"))  # Mark the change here, matey
print(bob_kaare)

# Avast! A modification is required in how we plunder 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 hold values like email an’ phone numbers, ye be forced to name the sub-value (e.g., address fer emails an’ number fer phone numbers) every time ye want tae grab it. This can get a bit tedious over land. Lucky fer yers, there’s a fix fer this: use the @property decorator inside a class, which lets ye pull the value straight from the object without havin’ ta specify the sub-value.

This brings on another trial though, ‘cause we need the __init__ function in the Person class. That’s because ye can’t use the same name fer 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 variable (starts with _ to indicate it be "private" like a locked chest)

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

    @property
    def verdi(self):
        return self._verdi.attributt  # Plunder the sub-value directly

# Test the code, ye scurvy dog
person = Person(name="Alice", verdi=EksempelVerdi("Noe tekst"))
print(person.verdi)  # Ye shall see: Noe tekst

Mark

Properties be unique fer they need no parameters, nor parentheses to run ‘em. In this example we plunder person.value without parens (not person.value()) even though it’s technically a function.

Alternate 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  # Plunder the sub-value directly

# Test ye code
person = Person(name="Alice", value="Some text")
print(person.value)  # Lootin': Some text

Hard Task 6 - Properties with logic (Optional)

Update yer Person by addin’ a new attribute named birthday. This here shall be of type datetime.date (from the datetime library).

Then ye shall create the followin’ properties:

  • Make a property age that calculates the person’s age based on birthday an’ today’s date.
  • Make a property is_adult that returns True if the soul is 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 birthdate an' current day's date"""
        today = date.today()
        age = today.year - self.birthday.year
        # Adjust down 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 mate 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 current date 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)