Production-ready Python
Tags: beginners, programming, python, productivity
Published: 2021-01-03 22:12:14

Below are 5 steps to take to build production-ready python applications.

## 1. Use Virtual Environment to Isolate Program.

The development environment in most cases is different from the production environment. For instance, the development environment for a program will be the programmer's machine (laptop, desktop or tablet). The production environment will be a virtual machine instance (AWS instance, Azure Virtual Machine, or Linode Standard) or a containerized instance(Docker, Kubernetes). To isolate the program in both environments with its Python version and modules, use the [Python Virtual Environment](https://docs.python.org/3/tutorial/venv.html) in both environments.

The example below shows how to create and activate an isolated environment

```
# Create virtual environment.
> python -m venv ./env

# Activate virtual environment on Windows
> ./env/script/activate.bat && ./env/scripts/activate

# Activate virtual environment on Mac
.env/bin/activate
```

Furthermore, required Python packages(with their required versions) can be installed and pushed to a file that'll hold the packages for the program.

```
# Install the Scrapy module via pip
(env) > pip install scrapy===2.4.1

# Push package to file
(env) > pip freeze > requirement.txt
```

The example above installs a specific package with the required version. The package is then pushed to a `requirement.txt` file using the `pip freeze` command. The `requirement.txt` file is used for specifying the Python packages the program requires, while the `pip freeze` command is used for outputting installed packages names with their correct version.

In a different environment, the virtual environment can be recreated, and the required packages with the required versions specified in the `requirement.txt` file can be installed. Installing the packages on a Mac is quite easy as it only requires running the `pip install` command with the `-r` switch on the `requirement.txt` file. But on windows, the packages have to installed by copying them from the requirement file list.

```
# Installing required packages on a Mac
(env)> pip intalled -r requirement.txt

# Installing required packages on a Windows
(env)> pip install scrapy===2.4.1
```

## 2. Use Config Files for Defining Deployment Environment.

The deployment environment for an app has to maintain proper configuration for the app to work correctly. The environment is where the app is run either in development or production. An app is said to be in **production** when it leaves the **development** stage.

The configuration setting of the production environment is different from that of the development environment. To modify a program on the production server, you'd have to run it on the development environment. The new changes are pushed to production.

The issue with these environments is the differences in configurations. The production environment requires different settings that may be hard to reproduce in the development environment. For instance, you have a program running on a web server with access to external data via [API](https://romeopeter.com/blog/api-first). To modify the code, you'd have to start the webserver container, set up API configuration and the appropriate keys needed to access the external data. These steps are unnecessary and time-consuming if all you want to do is modify a part of your program, then ensure everything works correctly.

A workaround to the above scenario is to modify your program at startup time to provide different functionalities depending on the deployment environment. Have a dedicated configuration for the program on both the development and production environment

```python
# settings.py

# Don't run with 'TESTING' turned on in production
TESTING = True

# API credentials should be kept secrete in production
API_CREDENTIAL = {
    "consumer_key": "XXXXXXXXXXXXXX",
    "consumer_secret": "XXXXXXXXXXXXXX",
    "access_token": "XXXXXXXXXXXXXX",
    "access_secret": "XXXXXXXXXXXXXX",
}

```

The TESTING constant is set to the Boolean value of True by default. It determines how the app runs on development and production. The API_CREDENTIAL constant is a key-value pair of the required API key.

Depending on the value of the TESTING variable, other modules in the app can import the settings.py file and determine how they implement their attributes.

```python
# main.py

import settings

from typing import List


class TestingAPI:
    """
    Use mock data in development
    """

    # ...

class RealAPI:
    """
    Uses real data via API in production
    """

    def __init__(self, api_credentials: dict) -> None:
        # ...

    # ...


if settings.TESTING:
    api = TestingAPI
else:
    api = RealAPI(settings.API_CREDENTIALS)
```



With the above example, modules should be customized to perform differently in deployment environments. Doing so makes it easy to skip the unnecessary reproduction code like API or database connection when it's not needed. Mock API data can be generated and injected into the program when testing or developing.

Another similar instance might be to make the app work differently based OS. Say the host server used in production is a different OS, say Linux and the one for development is a Windows, this might break the app because of the differences in OS type.

The Python `sys` module should be used to inspect the OS and determine its type.

```python
# main.py

import sys

class LinuxEnv:
    # ...
 
class WindowsEnv:
    # ...

if sys.platform.startswith('linux'):
    config = LinuxEnv
elif sys.platform.startwith('wind32'):
    config = WindowEnv
else:
    # ...
```

## 3. Debug Using  the `repr` Built-in Function

Basic debugging in Python is done using the `print` function. The print function returns a human-readable string version of whatever argument passed to it. If there is an error on a python program, the `print` function can be used to output how the state of the program changes while it runs to see where the error occurred.

The issue with this way of debugging is that the function can only output human-readable string version of the value supplied to it. It doesn't output what the **type** of the value is.

For instance, the `print` function output below doesn't make clear if the value type is a Number or a String.

```python
# Number
print(1024)

# String
print("1024")

# Human-readable output
>>>
1024
1024
```

 

The `print` function is for outputting human-readable values. This doesn't help at all when debugging.

Python provides the `repr` built-in function to return a **printable representation** of an object. It can be used in conjunction with the `print` statement know and ensure value types when debugging.

```python
# Number
print(repr(1024))

# String
print(repr("1024"))

# printable representation output
>>>
1024 # number
'1024' # string
```



The same result is reached employing the C-style `%r` format string, and the ` %` operator.

```python
# Number
print("%r" %1024)

# String
print("%r" %"1024")

# printable representation output
>>>
1024 # number
'1024' # string
```



#### Debugging Dynamic Objects

When debugging dynamic objects, using the human-readable function gives the same value as the `repr` function. That means the `print` function can be used because using `repr` on **Object** instances isn't helpful.

```python
class Person(object):
    def __init__(self, name, age, height):
        self.name = name
        self.age = age
        self.height = height
       
obj = Person('John', 25, "6'5")

print(obj)
repr(obj)

>>>
<__main__.Persson object at 0x0000011671395160> # human-readable string
'<__main__.Persson object at 0x0000011671395160>' # object representation
```



The are two ways to resolve this problem:

- Use the `__repr__` special method.
- Use object instance dictionary when you don't control the class.

The `__repr__` special method can only be used in classes that you control. It should define and return a string expression of the created object.

```python
class Person(object):
    def __init__(self, name, age, height):
        self.name = name
        self.age = age
        self.height = height
    
    def __repr__(self):
        return f'Person({self.name}, {self.age}, {self.height})'
       
obj = Person('John', 25, "6'5")

repr(obj)

>>>
"Person(John, 25, 6'5)"
```



When you don't have control over the class, use the `__dict__` special attribute to get access to the object instance dictionary. The `__dict__` attribute returns a dictionary of class attributes and methods.

```python
class Person(object):
    def __init__(self, name, age, height):
        self.name = name
        self.age = age
        self.height = height
    
    def __repr__(self):
        return f'Person({self.name}, {self.age}, {self.height})'
       
obj = Person('John', 25, "6'5")

print(obj__dict__)

>>>
{'name': 'John', 'age': 25, 'height': "6'5"}
```

## 4. Use Reusable Components

Write functions or classes that will be reused in other parts of the program to create a flow. For instance, A program that reads data from a data source(API, database, AWS s3), loads model from a pickle file, uses the model to generate predictions based on the dataset, and save the predictions in a database.

To achieve the instance above, the code responsible for handling the process could be divided into components rather than using a single function or class. Each component implies a different process that can be assembled with other components to create a pipeline for the required flow.

```python
def read_data_from_api(args, info):
    #...
    return data

def load_model(info):
    #... 
   	return model

def run_predictions(data, model):
	#...
    return predictions

def save_predictions_to_db(args, predictions):
	#...


def main:
   """Prediction pipeline"""

   data = load_data_from_api(args, info)
   model = load_model(args)
   Predictions = run_predictions(arg, data, model)
   save_predictions_to_db(args, predictions)
   #...
```



Benefits of using the component approach:

* Component can be reused in other pipelines
* Easy to improve and modify components overtime

In the code sample above, all four components are assembled as pipeline in the `mian()` function.

## 5. Test Program with `Unittest`

Python is a dynamically typed programming language. That means that it doesn't have a static type checker by default. Not having a static type checker often results in runtime errors.

All programs should be tested regardless of the programming language used, but Python is specifically [limited](https://romeopeter.com/pythons-limitations) as it has no type checking, at least not by default. Fortunately, the **unittest** module can be used to test python programs. Python dynamism makes it easy to write to test.

Testing is ensuring good code quality. It gives the programmer assurance that the program will work as expected when deployed. The responsible programmer should always build with testing in mind.

To use the built-in **unittest** module on your code, it has to be imported in a different python file. For instance, say you have a utility function defined in utils.py.

```python
# utils.py

def to_str(data):
    if isinstance(data, str):
		return data
	elif isinstance(data, bytes):
		return data.decode("utf-8")
	else:
		raise TypeError("Must supply string or byte. Found %r" %data)
```

To perform a test, you need to create a second file with the word `test` followed by the name of the file you want to test, which is the `utils.py` file. So the name of the file will be `test.utils.py`.

```python
#test_utils.py

from unittest import TestCase, main
from utils import to_str

class UtilsTestCase(TestCase):
    def test_to_str_bytes(self):
        # Verifies equality
    	self.assertEqual("hello", to_str(b’hello’))
    
    def test_to_str_str(self):
        # Verifies boolean expression
    	self.assertEqual("hello", to_str(‘hello’))

    def test_to_str_bad(self):
        #verifies exception is raised
    	self.assertRaises(TypeError, to_str, object())

if __name__ == "__main__":
    main()
```

Each test method begins with the word test. If a test method runs without raising any exception, then the test is successful. The tests above are organized according to test cases in the `TestCase` subclasses. The subclasses include helper methods for making assertion tests, such as `assertEqual`, `assertNotEqual`, `assertRaises`, and `assertTrue`.

> <small>NOTE:</small>
>
> To learn more about the testing and the unittest module, Click the link to the python documentation on testing. https://docs.python.org/3/library/unittest.html

## Conclusion

So there you have it, 5 guidelines on building production-ready python apps. Don't forget to check out the full [article](https://romeopeter.com/blog/setting-up-production-ready-python-apps/) to see all 7 steps.