Python Intermediate Topics
Object Oriented Programming, Custom Exceptions, and Best Practices.
Object Oriented Programming
Object Oriented Programming (OOP) is all about the idea of Encapsulation.
Encapsulation is a way to find related properties and behaviours in our program and create a blueprint out of them which we can use later.
Example
A scenario could be a program about managing the employees of a company. Each employee has some properties like name, age, and role. Some employees also have properties such as insurance and stock options plan.
There also some behaviours that each employee has, such as asking for a raise, PTO, or resign.
TRY IT YOURSELF
How would you go about writing this program with the knowledge you have so far? What are the limitations?
Hint: You can use Python dictionary to represent a person.
OOP Has Benefits
This is in Python, especifically, although most of them are language agnostic.
- DRY
- Enforce objects to have some properties and behaviours
- Simply working with complex data types like dictionaries and lists
- Extensible
- More readable
- Namespaces
- Inheritance
Classes
A Python class is a blueprint from which you can create instances (or objects). It contains the properties (fields) and behaviours (methods) you want your objects to have.
class ClassName:
# define properties and methods
Best Practice: Class names should be in CapitalCase.
__init__() Method
The __init__() method in a class is for defining and initializing properties that each object will get once initialized.
class Employee:
def __init__(self, name, age):
self.name = name
self.age = age
Like any other functions, class methods can accept arguments from the callers.
self
self represents the instance of a class, or the current object. Having self as the first parameter of class methods is a way to access the objects' properties
self is passed to class methods implicitly! That is, when calling class methods, we don't pass self.
Creating an Object
You can create an object off of a class by using the class name and passing the required arguments in the __init__() method.
employee_1 = Employee("Ross Geller", 30)
employee_2 = Employee("Rachel Green", 30)
Note that we're not passing anything for self. That's passed to the method by Python itself.
Properties and methods can be accessed using the dot (.) syntaxt.
print(employee_1.name)
Adding Methods
Methods are behaviours you want your objects to have. They are basically functions with the first argument being the object itself (self).
def submit_pto(self, hours):
print(f"{self.name} is requesting PTO for {hours} hours")
You can call the function using an object of the class and the dot . syntax.
employee_1.submit_pto(8)
Inheritance
You can create a class that inherits its properties and methods from another one and has some properties or methods on its own too.
class Cat(Animal):
# class Cat is inheriting from class Animal
TRY IT YOURSELF
Explain how OOP can offer the following benefits over the traditional (non-OOP) approach.
- DRY
- Enforce objects to have some properties and behaviours
- Simply working with complex data types like dictionaries and lists
- Extensible
- More readable
- Namespaces
- Inheritance
Testing
Testing your code is important because that's the only way you can be confident your code is going to work before merging it to one of the main branches.
Basically, with tests, you give your code a go before your end users do. This helps you find potential problems before your users experience them in production.
Types of Testing
There are different types of testing. Here are the most popular ones (there are more):
- Unit Testing
- Integration Testing
- End-to-End Testing
Unit Testing
Unit testing is a testing method focused around testing individual "units", or pieces of code.
The primary goal of unit testing is to make sure that a piece of code does what it’s supposed to.
The piece of code under test is usually a function.
We'll be talking about this type of testing in this course.
Integration Testing
Integration testing is a testing method focused on testing multiple components together.
The primary goal of integration testing is to make sure different components of software work together as expected.
End-to-End Testing (E2E)
System tests, or end-to-end (E2E) tests, are focused on vetting the behavior of a system from end-to-end.
The primary goal of end-to-end testing is to ensure the entire application or system as a whole behaves how we expect it to.
pytest
There are a number of third-party packages to unit test your Python code, including pytest, unittest, and testify. However, pytest is the most popular one and is the library we're going to use in this course.
pytest is not part of the Python standard library and needs to be installed before used. You can install it using pip install pytest.
Test Files
We write our unit tests in separate files. pytest requires the name of the test files to start with test_. Otherwise, it won't pick them up as files containing unit tests.
Unit Tests
Unit tests go into test files (files starting with test_). Unit tests are Python functions testing a piece of code (usually functions) in application code.
pytest requires the name of the unit test functions to start with test_. Otherwise, it won't run them.
Example of a Test File
Assuming we have a function named add(a, b) in a file named main.py, here's a test file to perform unit testing:
test_main.py
from main import add
def test_add():
a, b = 5, 10
result = add(a, b)
assert result == 15
assert
We use assertions to verify the logic of the piece of code we're testing. An assertion is a claim about a condition. We use the assert keyword to make an assertion.
Example: by assert result = 15 we're claiming that the value of result should be 15. If it's not, then the assertion is not correct and the unit test fails.
Commonly Used Assertions
| Assertion | Claim |
|---|---|
assert a == b | Assert that two values are equal. |
assert a != b | Assert that two values are not equal. |
assert a | Assert that a is True |
assert not a | Assert that a is False |
assert item in list | Assert that item is in a list |
assert item not in list | Assert that item is not in a list |
Running Tests
Once we have our test files and unit tests, we can run them using the pytest command. pytest will search the current directory (and its subdirectories) for files starting with test_. Then it will search for functions starting with test_ inside the files and run them.
pytest Cheat Sheet
pytest: will runpytest.pytest -k "substring": will only run tests that containsubstringin their names.pytest -s: will show program outputs such asprints.pytest -l: will show the value of local variables in case of an exception.pytest -v: will show verbose results (results with more information).
Linting
Linting your code has a number benefits:
- Helps identify errors
- Enforces coding standards
- Improves code quality by suggesting best practices
Python Linters
There are several third-party linters that you can use for linting your Python code. Here are a few:
pylintpytypeRuff
PyLint is the most popular one.
Using pylint
Like pytest, pylint is an external library that doesn't come with the Python standard library; hence, you need to install it first: pip install pylint.
Running pylint
You can run pylint using the pylint directory-name command. This will run pylint on all the Python files in tha directory.
Formatting Your Code
Although Python has an implicit formatting style (using indentation), there are still different ways to write Python code. Here's one example:
# one way
person = {"first_name": "Steve", "last_name": "Jobs", "is_alive": False}
# another way
person = {
"first_name": "Steve",
"last_name": "Jobs",
"is_alive": False
}
Using a Formatter
When working in a team, using a formatter helps a lot. Without a formatter, each developer could have a different coding style, which makes reviewing and reading each other's code difficult. Therefore, developers working on the same project are recommended to use a formatter to avoid such difficulties.
Python Formatters
There are several third-party formatters for Python. Here are a few:
blackisortautopep8
black is the most popular one. Current companies use black: Facebook, Dropbox, Lyft, Mozilla, Quora, Duolingo, Tesla.
Using black
Black is a third-party library that doesn't come with Python pre-installed. So, we need to install it first: pip install black.
Once installed, we can use the following command to format our code:
black source_file_or_directory
Using black in VSCode
You can also set black to be the default formatter of Python code in VSCode and run it each time you save your code.
Read more here.
Python Best Practices
A short list of best practices you're recommended to follow when writing Python programs.
The __name__ == "__main__" Expression
You're already familiar with Python modules (separate files with functions that you can import into other files). By using the __name__ == "__main__" you're letting the users of your modules to only run the scripts they want to and nothing else.
if __name__ == "__main__":
# some code
Order of imports
Try to have this order for your imports:
- Standard libraries (such as
json) - Third-party libraries (such as
requests) - Your own modules (such as
math_utilthat we created earlier)
Prefer Multiple Assignment Over Indexing
Python lets you unpack complex types like lists and tuples. Unpacking should be preferred because it makes code more readable.
area_perimeter = [16, 15]
# bad
area = area_perimeter[0]
perimeter = area_perimeter[1]
# good
area, perimeter = area_perimeter
Prefer enumerate over range in Loops
When you have a list of items, you can loop through the list and get each item plus the index of the item using range:
my_list = ["Calgary", "Toronto", "Edmonton"]
for idx in range(len(my_list)):
print(f"city number {idx} is {my_list[idx]}")
You can achieve the same thing with enumerate which gives you the next item and the index.
my_list = ["Calgary", "Toronto", "Edmonton"]
for idx, city in enumerate(my_list):
print(f"city number {idx} is {city}")
Use zip to Iterate Multiple Lists in Parallel
Sometimes, you have separate lists and want to loop over them at the same time and access items from both.
provinces = ["Alberta", "Ontario", "British Columbia"]
capitals = ["Edmonton", "Toronto", "Victoria"]
for province, capital in zip(provinces, capitals):
print(f"The capital of {provice} is {capital}.")
Gotcha: it only goes as far as the smallest list! Look at itertools.zip_longest().
Know How to Use Assignment Expression :=
Assignment Expression (:=) is a new syntax introduced in Python 3.8. The expression is most useful for doing an assignment and perform a conditional test check at the same time:
cities = ["Calgary", "Edmonton"]
# bad
first_city = cities[0]
if first_city == "Calgary":
# do stuff
# better
if (first_city := cities[0]) == "Calgary":
# do stuff
Use Catch-all Expression Over Slicing
You can divide a list to different sections using slicing:
cities = ["Calgary", "Edmonton", "Victoria", "Vancouver", "Waterloo", "Montreal"]
most_important = cities[0]
second_important = cities[1]
the_rest = cities[2:]
Thats is verbose and hard to read. You can use unpacking and catch-all expression (*) instead.
cities = ["Calgary", "Edmonton", "Victoria", "Vancouver", "Waterloo", "Montreal"]
most_important, second_important, *the_rest = cities
Remember Functions Are First-class Citizens
Functions in Python are first-class citizens, meaning that you can do almost everything with them as you can with variables: assing them into variables, pass them into another functions, return by a function, stores in lists, etc.
def control_unit(fn, **kwargs):
return fn(**kwargs)
def add(a, b):
return a + b
def subtract(a,b):
return a - b
print(control_unit(add, a=5, b=10))
print(control_unit(subtract, a=5, b=10))
Use lambda For One-line One-time Functions
There are times that you need to pass a function as an argument to another function, but that's it! You're not going to use that function anywhere else. Python's lambda enables you to do that.
print(control_unit(lambda a,b:a+b, a=5, b=10))
Sort Based on Custom Criteria Using the key Parameter
You can use the sort or sorted with list of items that have a natural order (e.g. numbers, strings). But what about objects of a class?
class Person:
# has 'name' and 'age' properties
person_1 = Person("Alice", 28)
person_2 = Person("John", 30)
people = [person_1, person_2]
people.sort()
# sort based on age
people.sort(key=lambda x:x.age)
Use get() to Look for a Key in a Dictionary
Using the dictionary_name["key"] to get a key from a dictionary has the potential of raising a KeyError exception, which you should handle to make sure your program doesn't crash.
Use get() instead as it doesn't raise any exceptions. You're also able to provide a default value as the second argument.
my_dict = {"name": "Ross"}
age = my_dict["age"] # exception!
age = my_dict.get("age") # returns None but no exception
age = my_dict.get("age", 0) # returns 0
Raise Exception Instead of Printing Errors
When a function runs into a situation where it can't perform properly, it's always a good idea to raise an exception and let the caller know about it, other than simply printing out or returning a message as that'd be hard to decode.
# bad
def set_age(self, age):
if age < 0:
return "invalid age"
self.age = age
return "OK"
# better
def set_age(self, age):
if age < 0:
raise ValueError("invalid age")
self.age = age
Know Python's Built-in Exceptions
You can see the exceptions that come with the Python library here.
The popular and generic ones:
ValueErrorKeyErrorException
Create Custom Exception to Improve Readability
If you need to raise an exception that doesn't come with Python, you can create a new one with a descriptive name by creating a class that inherits from the Exception class:
# bad
def get_record(record_id):
# ...
raise ValueError("record doesn't exist")
# good
class RecordNotExist(Exception):
pass
def get_record(record_id):
# ...
raise RecordNotExist(f"record {record_id} doesn't exist")
Use Enums When You Need Distinct Options Regardless of Values
Sometimes all you want is a list of distinct things. For example, you want to mark a process as COMPLETED, IN_PROGRESS, or STOPPED. As long as you can distinguish between the three states, you're good. You don't care about the values.
In cases like this, you can use inherit from the Enum class and create distinct options:
from enum import Enum
class Status(Enum):
COMPLETED = 1
IN_PROGRESS = "banana"
STOPPED = False
process.status = Staus.STOPPED
Write Docstrings for Every Function, Class, and Module
You should always provide documentation about your code, whether that's a function, class, or a module.
We add docstrings using the """.
- For modules: that goes to the first line of the file
- For functions (and methods): the first line after the function definition.
- For classes: the first line after the class definition.
Follow Docstring Patterns
There are certain patterns that Python developers use when it comes to docstrings. Here's an example for a function:
def find_anagrams(word, dictionary):
"""Finds all anagrams for a word.
Args:
word: String of the target word.
dictionary: dictionary of words as keys and definitions as values.
Returns:
List of anagrams that were found. Empty if
none were found.
Raises:
ValueError if word is not a string.
"""
Check Documentation with pydoc
You can see run a server that shows all the Python documentation, plus your own using the pydoc library.
pydoc -p 3337
Comprehension
There's a shorter syntax in Python when you want to create a list, dictionary, or tuple based on the values of an existing list, dictionary, tuple, etc.
numbers = [1,2,3,4]
squared = []
for number in numbers:
squared.append(number**2)
print(squared) # [1, 4, 9, 16]
# better
squared = [number**2 for number in numbers]
print(squared) # [1, 4, 9, 16]
List Comprehension
When you want to create a new list based on the values of an iterable.
Syntax:
new_list = [expression for item in iterable if condition == True]
Example:
numbers = [1,2,3,4]
even_squared = [number**2 for number in numbers if number % 2 == 0]
print(squared) # [4, 16]
Dictionary Comprehension
When you want to create a new dictionary based on keys and values of an iterable.
Syntax
new_dict = [expression for item in iterable if condition == True]
Example:
names = ["Ross", "Chandler", "Rachel", "Monica"]
people = {f"person_{idx}": person for idx, person in enumerate(names, 1)}
print(people)
# {'person_1': 'Ross', 'person_2': 'Chandler', 'person_3': 'Rachel', 'person_4': 'Monica'}
Testing & Linting
Always write unit tests for your code and use a linter to check for potential errors and areas of improvements.