Data validation n' classes

Skip to content

This here’s a machine-translated text that might contain some errors!

In Python (and most other tongues, mind ya), a fella can build his own contraptions, with their own values, rules, and doin’s. We call these classes (classes in English, naturally). We use classes to gather related data and doin’s into one unit, like an Order class that holds data like order_id, customer_name, products, and doin’s like add_product(), calculate_total(), and so on.

Dictionaries (JSON) versus Classes (Objects)

JSON (JavaScript Object Notation) is a way to store and transfer data regardless of programming languages used on either side; while classes represent structures within specific codebases/languages themselves..
When sending stuff across networks OR saving it into files - ya usually use json… Or maybe tables inside databases too depending what fits your needs best right now though honestly speaking most people stick w/ one or another based purely personal preference tbh lollll 😅🤣😂💀☠️⚔️🗡️💘❤‍🔥❄️♨️🌊🎢🏰👻💩🐲🦖🧬🕹️💾 💿 📼 🔋 ⛽ ☁️ ❇️ ✴️ ▪️▫️◽◾▪︎▫︎ ◈◇○◎●〇◆◇▽△▲▼■□♦♡♥♣♠♪♭♮➜↪⇵ ↔ ↯ ↳ ⇝ ➟ ㉱⑆㊃㊙℞™©®§¶†‡⁂※‼⁉ ⁇ ❓✔✅❗❌✳❶ⅡIVXLI X XI IX IV V VI VII VIII III II I

So when working with structured data INSIDE YOUR CODE itself then THAT’S where YOU SHOULD DEFINITELY be using CLASSES instead!!

In this here module, we’re gonna take a look at how we can use classes to validate data.

The easiest way is to use @dataclass from the dataclasses library. This here lets us skip writin’ a whole lotta boilerplate code to create a class. (Like them built-in __init__ and __repr__ (representation) functions).

Here’s an 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__?

Example usin’ a dataclass decorator, which gets ya the same result 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)  # Output: Car(make='Toyota', model='Corolla', year=2020)

Simple Task 1 - Create a class

Create a class named Person. This class should have the following attributes:

  • name: The person’s name
  • eye_color: The color of the person’s eyes
  • phone_number: The phone number of the person
  • email: The email address of the person

Instantiate an object of the Person class using valid values for all properties, similar to the example below.

@dataclass
class Person:
    ... # Yer 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 Data Class for Person

Here’s one way you could solve it:

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 ain’t added no validatin’. That means we can rustle up a Person with some bad values, like so:

@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')

This here’s (potentially) troublesome, and might just cause some technical debt down the line. Luckily, there’s simple ways to add validation to classes.

We’re gonna start by lookin’ at email validation. There’s built-in libraries in Python that can help us with this, but seein’ as we’re doin’ this to learn, we’re gonna make our own simple validation, by makin’ a new class for “Email”, and lookin’ at a __post_init__ function (just for dataclasses).

Note

We can also do this within the Person class itself, but it’s often better to create separate classes for things that can be reused.

Example of a post_init function
from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        print(f"Validating email: {self.address}")
        # Your code here

Now, fer a simple email validation, we can, fer instance, check if the email contains both the @ and . characters. Optionally, ya can check if the email matches a regex pattern. (More advanced, but feel free to search the web!)

Solution: Code for simple email validation

Here’s a possible solution—we use exceptions to “crash” the app if an invalid email is passed; this immediately stops execution and throws 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 email address: {self.address}")
        if "." not in self.address.split("@")[1]:
            raise ValueError(f"Domain part of '{self.address}' must contain '.' after split by '@'.")

        return super().__init__() 

# Testing code below... (try it out!)    
test_203456789@gmail_com_validated_as_True_or_False_based_on_input_string_passed_into_constructor_method_call_above_this_line_here_right_now_in_time_space_continuum_universe_multiverse_parallel_dimensional_reality_branches_alternate_timelines_et_cetera_blahblahbleh!!! YEE-HAW!! 🤠🐎✨💻⚡️😂🔥

</span>

<span class="turtletranslate-section" data-turtletranslate-type="article" data-turtletranslate-index="17" data-turtletranslate-checksum="f7695cf89f4d84ab">

## ![Medium](attachments/diff2.webp#center){width="48"} Task 3 - Phone Number Validation

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

</span>

<span class="turtletranslate-section" data-turtletranslate-type="blockquote" data-turtletranslate-index="18" data-turtletranslate-checksum="f5b7806eaf594eb2">

> [!DANGER]+ Challenge with phone validation!
> Can ya fix the validation for phone numbers to accept both letters (str) and numbers (int)? For example, `12345678` and `"12345678"` should both be valid.
> 
> Also, try addin' country codes as an attribute (sub-value to the class). For example, `47` for Norway, `46` for Sweden

</span>

<span class="turtletranslate-section" data-turtletranslate-type="article" data-turtletranslate-index="19" data-turtletranslate-checksum="ab9a49d9fa94f409">

## ![Medium](attachments/diff2.webp#center){width="48"} Task 4 - Use Validation in the Person Class

Now that we've built validation for email and phone number, we can use 'em in the `Person` class.

</span>

<span class="turtletranslate-section" data-turtletranslate-type="codefence" data-turtletranslate-index="20" data-turtletranslate-checksum="60f7fbe3008a09ba">

```python
@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: PhoneNumber  # Use the PhoneNumber class
    email: Email               # Use the Email class

[!IMPORTANT] New challenge arises!
Now that we’ve changed the Person class to use PhoneNumber and Email classes, we also need to change how we instantiate (create) a Person. We must first create a PhoneNumber object and an Email object before creating 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 represent values like email and phone numbers, we gotta specify the sub-value (like address for email and number for phone number) every time we wanna get the value out. This can get a mite cumbersome in the long run. Luckily, there’s a solution to this, by usin’ the @property decorator in a class, which lets us get the value straight from the object, without havin’ to specify the sub-value.

This does bring on another challenge, though, and that’s we need the __init__ function in the Person class. This is ‘cause we can’t use the same name for both a property and an attribute in 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] Note
Properties are unique because they don’t require parameters nor parentheses when invoked. In this example, we access person.value without brackets (not person.value()) even though it’s technically a function under the hood.

[!TIP] Alternative example that accepts both str and EksempelVerdi

@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  # Retrieves 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 called birthday. This here should be of type datetime.date (from the datetime library).

Then ya gotta make the followin’ properties:
- Make a property age that calculates the person’s age based on birthday and today’s date.
- Make a property is_adult that returns True if the person is 18 years or older, otherwise False.

[!HINT]- 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
        # Subtract one if they haven'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 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 15th, 2023
print(person.is_adult)  # True

[!DANGER] Another challenge!
Can ya manage to get instantiation of 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)