Python Intermediate: Modules and packages

Modules and packages, together, lay the foundation for constructing well-organized, scalable, and efficient Python applications, where code can be compartmentalized and accessed with ease, ultimately enhancing the developer's productivity and the codebase's overall readability.

Python Intermediate: Modules and packages

For more posts about Python, follow the link below.๐Ÿ‘‡๐Ÿ‘‡๐Ÿ‘‡

Exploring Python
Python is frequently utilized in creating websites and software, as well as for automating tasks, analyzing data, and visualizing information.

Modules and packages are essential components of Python's modular programming paradigm, enabling developers to organize, structure, and manage their code efficiently. They offer a powerful mechanism for building complex applications, allowing developers to create a well-organized ecosystem of modules and subpackages. In this dynamic Python landscape, modules and packages play a vital role in promoting code reusability, collaboration, and the development of scalable software solutions.

Python Intermediate: Iterators, list comprehensions, and generator expressions
Mastering iterators, list comprehensions, and generator expressions is essential. We will take on a journey through these fundamental Python constructs, revealing how they simplify code, boost data processing, and enhance code readability.

Modules

Modules provide a powerful mechanism for structuring and organizing code. They allow us to group related functions, classes, and variables into separate files, making our code more manageable and reusable. Modules help avoid naming conflicts, promote code isolation, and enable collaboration by allowing different parts of a program to be developed independently.

There are several ways to import modules, allowing us to control how we access the functions, classes, and variables within a module. Here are the most common ways to import modules.

Import the Whole Module. The simplest way to import a module is by using the import keyword followed by the module name. This makes all the functions, classes, and variables in the module available under the module's name.

import my_module
result = my_module.some_function()

Import Specific Functions or Variables. We can also import specific functions or variables from a module. This way, we can use them directly without referring to the module name.

from my_module import some_function, my_variable
result = some_function()

Import with an Alias. We can provide an alias for a module or specific items within it. This can make our code more concise and readable, especially when dealing with modules with long names.

import my_module as mm
result = mm.some_function()

We can also use an alias when importing specific functions or variables.

from my_module import some_function as sf
result = sf()

Import Everything. While we can use a wildcard (*) to import all items from a module, it is generally not recommended. This can make our code less readable and lead to naming conflicts if multiple modules have items with the same name.

from my_module import *
result = some_function()

Conditional Import Using importlib. In some situations, we might need to import a module conditionally at runtime. The importlib library provides functions to do this dynamically.

import importlib

if some_condition:
    my_module = importlib.import_module('my_module')
    result = my_module.some_function()

These are the primary ways to import modules in Python. The choice of method depends on our specific needs and coding style. It's generally a good practice to import only what we need to keep our code clean and avoid naming conflicts, especially in larger projects.

Built-in modules

Python provides built-in pre-existing libraries. These modules offer a wide range of functionalities and tools for various tasks, such as mathematical operations, file handling, network communication, data manipulation, and more. They are an essential part of the standard library, and developers can use them without needing to install any additional packages.

math ๐Ÿงฎ

The math module offers a comprehensive set of mathematical functions, including trigonometric, logarithmic, and arithmetic operations. It's useful for tasks that involve complex mathematical calculations.

>>> import math
>>> math.sqrt(25)
5
>>> math.sin(math.pi / 2)
1.0

random ๐ŸŽฒ

The random module allows us to generate pseudo-random numbers. It's handy for simulations, games, and other scenarios where randomness is required.

>>> import random
>>> print(random.randint(1, 10))
4
๐Ÿ’ก
We have already used the random module when implementing "Cows and Bulls" game!

datetime ๐Ÿ“†

The datetime module provides classes for working with dates and times. We can use it to manipulate, format, and calculate dates and times.

>>> from datetime import datetime
>>> datetime.now()
datetime.datetime(2023, 10, 31, 20, 0, 14, 205257)
>>> now = datetime.now()
>>> print(now)
2023-10-31 20:00:37.806677

os ๐Ÿ’พ

The os module allows us to interact with the operating system, including functions to manage files and directories, check file existence, and execute system commands.

>>> import os
>>> os.listdir('.')
['site-packages-3', 'Welcome.md', 'site-packages-2', 'main2.py', 'site-packages', 'main.py', 'Examples']

sys ๐Ÿ’ป

The sys module provides access to system-specific parameters and functions. It's commonly used to manipulate the Python interpreter and command-line arguments.

>>> import sys
>>> sys.platform
'ios'

json ๐Ÿ“„

The json module is used for encoding and decoding JSON (JavaScript Object Notation) data. It's invaluable for working with APIs, configuration files, and data interchange.

>>> import json
>>> data = {'name': 'John', 'age': 30}
>>> json.dumps(data)
'{"name": "John", "age": 30}'

socket ๐Ÿš€

The socket module allows us to create and manage network connections. We can use it to implement network protocols, client-server applications, and more.

>>> import socket
>>> server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> server_socket.bind(('127.0.0.1', 8080))

re โ ป

The re module provides support for regular expressions. It allows us to search, match, and manipulate text using complex pattern matching.

>>> import re
>>> pattern = r'\d{3}-\d{2}-\d{4}'
>>> text = f'My Social Security Number is 123-45-6789.'
>>> re.search(pattern, text)
<re.Match object; span=(29, 40), match='123-45-6789'>

Custom modules

Custom modules in Python are user-defined collections of functions, classes, and variables encapsulated within individual Python files with a .py extension. They enable developers to create their own libraries of functions tailored to specific tasks, making code more modular and maintainable.

Creating a custom module is a straightforward process. A custom module is essentially a Python script. To create one, start by writing our desired functions or classes in a file and save it with a .py extension. For example, if we want to create a module for mathematical operations, we can write functions for addition, subtraction, multiplication, etc.

def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

my_math_module.py

Once we've defined our module's content, we can import and use it in other scripts by simply using the import statement.

>>> import my_math_module
>>> my_math_module.add(5, 3)
8
>>> my_math_module.subtract(10, 4)
6

main.py

How can we use modules in our case? Let's recall our Car class from the previous post.

Python Intermediate: Classes, objects, and inheritance
Building upon these foundational concepts, classes, and objects allow us to structure our code in a more organized and modular manner. They promote code reusability and help model real-world entities in a way thatโ€™s intuitive and maintainable.

We already have defined Car and Ambulance classes there. It is likely to happen when the project grows, additional vehicles will pop up. Let's imagine there is a new requirement to introduce a TowTrack vehicle. The class hierarchy diagram is as follows.

We are already familiar with Car and Ambulance code.

class Car:
    def __init__(self, make: str, model: str):
        self.__make: str = make
        self.__model: str = model

    def __str__(self) -> str:
        return f'{self.__make} {self.__model}'

    def __repr__(self) -> str:
        return f'{self.__make} {self.__model}'

    def drive(self):
        print(f'Driving {self.__make}: {self.__model}')

    def is_model(self, model: str) -> bool:
        return model in self.__model


class Ambulance(Car):
    def __init__(self, make: str, model: str, sound: str = f'Wee woo!!!'):
        super().__init__(make, model)
        self.__sound: str = sound

    def siren(self):
        print(f'Siren: {self.__sound}')

Here comes the new vehicle type, a TowTrack.

class TowTruck(Car):
    def __init__(self, make: str, model: str):
        super().__init__(make, model)

    def tow(self, car: Car):
        print(f'Driving {self.__make}: {self.__model} and towing: {car}')

The new vehicle provides the ability to tow other cars, which might be pretty crucial when they break down. Now it might be a good choice to put all those cars in one separate module. Let's name it vehicle.

class Car:
    def __init__(self, make: str, model: str):
        self.__make: str = make
        self.__model: str = model

    def __str__(self) -> str:
        return f'{self.__make} {self.__model}'

    def __repr__(self) -> str:
        return f'{self.__make} {self.__model}'

    def drive(self):
        print(f'Driving {self.__make}: {self.__model}')

    def is_model(self, model: str) -> bool:
        return model in self.__model


class Ambulance(Car):
    def __init__(self, make: str, model: str, sound: str = f'Wee woo!!!'):
        super().__init__(make, model)
        self.__sound: str = sound

    def siren(self):
        print(f'Siren: {self.__sound}')


class TowTruck(Car):
    def __init__(self, make: str, model: str):
        super().__init__(make, model)

    def tow(self, car: Car):
        print(f'Driving {self.__make}: {self.__model} and towing: {car}')

vehicle.py

Now in the main script, we just have to import and use them.

from vehicle import Ambulance, Car, TowTruck


def main():
    vehicles = [Car(f'Toyota', 'Camry'),
                Ambulance(f'Toyota', 'Corolla'),
                TowTruck(f'Toyota', f'Prius')]
    print(vehicles)


if __name__ == "__main__":
    main()

Running the main script prints us a list of cars.

โฏ python3 main.py
[Toyota Camry, Toyota Corolla, Toyota Prius]

Packages

Packages in Python are a way to organize and structure our code into directories and subdirectories, providing a hierarchical structure for our modules. They are used to group related modules together and make it easier to manage large projects.

What is a Package? A package is a directory that contains a special __init.py__ file (which can be empty) and one or more module files. The __init.py__ file indicates to Python that the directory should be treated as a package, and it can also contain package-level initialization code. To create a package, we simply need to create a directory and place an __init.py__ file inside it. We can have multiple modules within this directory.

my_package/
โ”œโ”€โ”€ __init__.py
โ”œโ”€โ”€ module1.py
โ”œโ”€โ”€ module2.py

Here, my_package is the package, and module1.py and module2.py are modules within the package. We can import modules from a package using dot notation. For example, if we have a module named module1 within the my_package package, we can import it as follows.

from my_package import module1

We can also import specific functions, classes, or variables from the module using dot notation.

from my_package.module1 import my_function

Nested Packages. Packages can be nested within other packages, creating a hierarchical structure. For example, we can have a package within a package.

my_package/
โ”œโ”€โ”€ __init__.py
โ”œโ”€โ”€ module1.py
โ””โ”€โ”€ subpackage/
    โ”œโ”€โ”€ __init__.py
    โ”œโ”€โ”€ module3.py

We can access modules from the subpackage as follows.

from my_package.subpackage import module3

Package Initialization. The __init.py__ file in a package can contain the initialization code that will be executed when the package is imported. This is useful for setting package-wide configurations or performing setup tasks. We can also define variables or functions within the __init.py__ file to make them accessible as package-level resources.

Within a package, we can use relative imports to import modules or submodules from the same package. For example, if we have a module module4 in the same package as module1, we can use a relative import.

from . import module4

The dot . represents the current package.

__all__ Attribute. We can specify a list of modules or module names in the __init.py__ file using the __all__ attribute. This attribute tells Python which modules are considered public and should be imported when someone uses a wildcard import (from package import *).

__all__ = ['module1', 'module2']

my_package/__init__.py

Packages help keep our code organized and modular especially when managing complex projects.

We can make use of packages in our project as well. Let's create the transporation package. Then move vehicle.py there and create the __init.py__ file.

class Car:
    def __init__(self, make: str, model: str):
        self.__make: str = make
        self.__model: str = model

    def __str__(self) -> str:
        return f'{self.__make} {self.__model}'

    def __repr__(self) -> str:
        return f'{self.__make} {self.__model}'

    def drive(self):
        print(f'Driving {self.__make}: {self.__model}')

    def is_model(self, model: str) -> bool:
        return model in self.__model


class Ambulance(Car):
    def __init__(self, make: str, model: str, sound: str = f'Wee woo!!!'):
        super().__init__(make, model)
        self.__sound: str = sound

    def siren(self):
        print(f'Siren: {self.__sound}')


class TowTruck(Car):
    def __init__(self, make: str, model: str):
        super().__init__(make, model)

    def tow(self, car: Car):
        print(f'Driving {self.__make}: {self.__model} and towing: {car}')

transportation/vehicle.py

When exploring __dunder__ methods, we created ParkingLot class. Now we are going to use it again. If you need a refresher on __dunder__ methods, grab the link.๐Ÿ‘‡๐Ÿ‘‡๐Ÿ‘‡

Python Intermediate: Dunder methods
โ€œDunderโ€ is short for โ€œdouble underscore,โ€ and it is commonly used in the context of Python programming.

Let's create area.py module inside transporation package and place ParkingLot class inside.

from transportation.vehicle import Car


class ParkingLot:
    def __init__(self, spots: int):
        self.__spots: list[Car] = list()
        for _ in range(spots):
            self.__spots.append(None)

    def __len__(self) -> int:
        return len(self.__spots)

    def __str__(self) -> str:
        occupancy = f'Space occupancy: \n'
        for index, spot in enumerate(self.__spots):
            occupancy += f'Spot {index} is '
            if spot:
                occupancy += f'BUSY, parked car: {spot}\n'
            else:
                occupancy += f'FREE\n'
        return occupancy

    def __setitem__(self, index: int, car: Car):
        self.__spots[index] = car

    def __delitem__(self, index: int):
        self.__spots[index] = None

transportation/area.py

Now we can make a small update to the __init.py__ file.

__all__ = ['area', 'vehicle']

Once we are done, the project layout should be as follows.

transportation/
โ”œโ”€โ”€ __init__.py
โ”œโ”€โ”€ area.py
โ”œโ”€โ”€ vehicle.py
main.py

Now we can use our freshly created package in the main.py script.

from transportation.vehicle import Ambulance, Car, TowTruck
from transportation.area import ParkingLot


def main():
    vehicles = [Car(f'Toyota', 'Camry'),
                Ambulance(f'Toyota', 'Corolla'),
                TowTruck(f'Toyota', f'Prius')]
    parking_lot = ParkingLot(10)
    for index, v in enumerate(vehicles):
        parking_lot[index] = v
    print(parking_lot)


if __name__ == "__main__":
    main()

main.py

We are ready to run it! ๐Ÿš€

โฏ python3 main.py
Space occupancy: 
Spot 0 is BUSY, parked car: Toyota Camry
Spot 1 is BUSY, parked car: Toyota Corolla
Spot 2 is BUSY, parked car: Toyota Prius
Spot 3 is FREE
Spot 4 is FREE
Spot 5 is FREE
Spot 6 is FREE
Spot 7 is FREE
Spot 8 is FREE
Spot 9 is FREE

Summary

Modules and packages are indispensable tools for structuring and managing code. Modules allow developers to encapsulate and reuse functions, classes, and variables, enhancing code readability and maintainability. Packages, on the other hand, offer a hierarchical structure for organizing modules, making them essential for handling more extensive projects and promoting a modular and collaborative development approach. Together, modules and packages provide a powerful foundation for constructing well-organized, scalable, and efficient Python applications, fostering code reusability, collaboration, and the creation of easily maintainable software solutions.

Whatโ€™s next?

Subscribe to receive more posts like this directly to your email!

Ideas๐Ÿ’ก, questionsโ”โ“ ? Feel free to start the discussion ๐Ÿ—ฃ๏ธ๐ŸŽ™๏ธ!

Python Intermediate: Decorators
Decorators are a fundamental tool for achieving separation of concerns and keeping code concise and readable. They provide a clean and reusable way to add functionality to existing code without changing its core logic.

Ready for challenges? ๐Ÿ‘‡๐Ÿ‘‡๐Ÿ‘‡

Python Intermediate: Brain teasers
In the world of programming, the road to expertise is paved with challenges. Donโ€™t shy away from solving programming problems; instead, embrace them as opportunities for growth.