Python dev requirements

However, to become an experienced Python developer, one must possess a thorough understanding and expertise in the following specific and advanced topics.

1. Deployment

This topic delves into the experience of a developer when deploying a Python application for production. While there are countless Python applications out there, we'll focus on one scenario: assume you have a Python web application that's up and running smoothly; how do you go about deploying it in a way that makes it accessible to people all around the world?

To answer this question comprehensively, let us understand precisely what happens when sending an HTPP request to our Python application. The diagram below shows this process.

Diagram 1

As you can see, the users send HTTP requests to the webservers. The main reasons for having web servers like Apache or Werkzeug are:

  1. Receive, process, and handle HTTP requests.

  2. Reverse Proxy as a Load Balancer: They distribute the coming requests among several servers running the Python application (several backends) or among workers (different processes or threads). This makes our application scalable.

  3. To Serve the Static Content: Static files are CSS, JavaScript, HTML, or images or videos. The web servers can load them directly from the memory or disk, and it leads to improving performance.

  4. Increase security: They can provide several security features such as access control, authentication, etc.

In the second phase of this process, when the web servers are ready to send requests to the Python frameworks, the Web Server Gateway Interface (WSGI) comes into play. PEP 333 introduced this interface, which acts as an intermediary between web servers and Python frameworks, ensuring they communicate in a consistent, organized, and predictable way, regardless of their implementation details. Some of the most popular WSGI servers include Gunicorn and uWSGI.

Please be aware that we have another concept named ASGI (Asynchronous Server Gateway Interface). Its main difference from WSGI is that it can simultaneously handle multiple asynchronous requests and responses. One library for ASGI is Uvicorn, built on top of the asyncio library and designed to work with Python 3.6 and newer.

The last step in the process is for the WSGI server to serve the Python framework application. It's important to note that each Python web framework includes a server component that receives client requests, processes them, and ultimately returns a response. Some Python web frameworks, like Django and Flask, come with built-in servers suitable for development purposes (such as running the application locally). However, it's not recommended to use these servers in production environments.

Now it is clear what we need to be able to deploy and make our Python app scalable and available in production.

2. Python as a functional and object-oriented language

Python is not a “pure” functional programming language. However, it supports many concepts that are related to Functional Programming Paradigm. Some are listed below.

  1. Functions can be passed as arguments and returned from the functions.
from typing import Callable

def f1(f2: Callable, num1: float, num2: float):
    s = f2(num1, num2)
    return s

def f2(num1: float, num2: float):
    return num1 // num2

print(f1(f2, 40, 3)) #13
  1. Lambda or anonymous functions. In the example below, we pass a lambda function to sort the dictionaries based on the name key alphabetically. If the name is the same for some dictionaries, we sort according to the age key.
dicts_list = [{'name': "Armin", 'age': 25}, {"name": "Jack", "age": 10}, {"name": "Armin", "age": 27}]

dicts_list.sort(key=lambda d: (d["name"], d["age"])) # [{'name': 'Armin', 'age': 25}, {'name': 'Armin', 'age': 27}, {'name': 'Jack', 'age': 10}]
  1. It has several immutable objects such as tuples, strings, frozen sets, etc. This concept is critical in functional programming, where immutability is favored over mutability.

  2. Having functions like map(), filter().

nums = [n for n in range(1, 6, 1)]

even = list(filter(lambda n: n % 2 == 0, nums))  # [2, 4, 6, 8]

cube = list(map(lambda n: n ** 3, nums))  # [1, 8, 27, 64, 125]

Python is an Object-Oriented Programming (OOP) language as well. You should expect your candidate to know these things below that are all related to Object-Oriented Paradigm.

  1. Why do we use OOP?

The main reason is to have maintainable, structured, reusable, modular code.

  1. What are the OOP principles implemented in Python?
  • Encapsulation: It is achieved by using private and protected variables and methods.

  • Polymorphism: It can be achieved by using method overriding and method overloading.

  • Inheritance: When a class can inherit methods and variables of one or more classes.

  • Abstraction: Abstraction is achieved by using abstract classes and interfaces.

  1. What is the Diamond Problem in Python?

This is related to MRO (Method Resolution Order), with which Python looks for method and attribute definitions in a class hierarchy when multiple inheritances are involved.

class A:
    def f(self):

class B(A):
    def f(self):

class D(A):
    def f(self):

class C(B, D):

c = C()

To understand the code above better, look at the diagram below.

Diagram 2

When we call c.f(), the MRO for this function is C, B, D, A, object, which is why the answer for the code above is “B.”

  1. What are some OOP design patterns in Python?

There are many design patterns. An expert in Python must know at least these.

SOLID principles:

  • Single Responsibility: Each class must have one responsibility.

  • Open-Closed: We use OOP principles to extend the code rather than change it.

  • Liskov Substitution: A subclass should be substitutable for its base class without any change in the behavior.

  • Interface Segregation: A subclass must inherit from an interface that it uses.

  • Dependency Inversion: We decouple classes and modules from specific implementations using interfaces and abstraction.

Factory pattern: A factory class or method that is responsible for creating and returning objects of a specified type.

Singleton pattern: When only one instance of a class gets created and used through the application.

3. When to use multiprocessing, subprocess, multithreading, and asyncio?

The answer to this question depends heavily on the tasks that your Python application is designed to perform.

We can categorize machine tasks into two main types based on the resources they require: IO-bound and CPU-bound.

IO-bound tasks require a lot of input/output operations, such as reading and writing data to disks or networks, such as writing to/reading from a database, sending an email, sending an API request, etc. On the other hand, CPU-bound tasks require a lot of processing power to perform complex computations. Tasks like Image, video, audio processing, data compression, decompression, and cryptography are all CPU-bound operations.

In the context of programming, we usually use Parallel Programming when dealing with CPU-bound tasks, and the packages for that in Python are multiprocessing and subprocess. The main difference between these two is that multiprocessing is used for running multiple processes (therefore, multiple Python Interpreters). In contrast, subprocess is used for spawning new processes to run commands or to run external programs from within the current Python program.

For I/O-bound tasks, we can use concurrent programming. In Python, we use multithreading and asyncio packages to achieve that. The major difference between them comes from the nature of these two. Asyncio is single-threaded and works based on the concept of the event loop. An event loop behaves like a queue data structure to which we can register the coroutines. This differs from multithreading, by which we can create multiple threads and run them simultaneously.

4. Python’s garbage collection

Before answering this question, please pay attention that the specific implementation of the garbage collector may vary between different versions of Python, and other implementations, for example, Jython or IronPython, may have different GC strategies altogether.

Reference counting is the primary topic we should talk about regarding GC of Python.

As you know, everything in Python is an object. When we declare var = 123, what happens is that an object of type integer gets created (on the private heap), and its reference count is 1 for now. When we assign this variable to another variable, its reference count increments by 1. When passing it as an argument to functions, its reference count first increases by one and then decreases by one as soon as the function returns.

The code snippet below illustrates incrementing and decrementing the reference count of object a.

import ctypes
from typing import List

# Function to show the reference count of a specific memory location address.
def count_ref(memory_address: int) -> int:
    return ctypes.c_long.from_address(memory_address).value

a = [1, 2, 3]

assert count_ref(id(a)) == 1

a1 = a
a2 = [a, [4, 5, 6]]

assert count_ref(id(a)) == 3

def my_f(l: List):
    assert count_ref(id(a)) == 4


assert count_ref((id(a))) == 3

a2 = [10, 100]

assert count_ref((id(a))) == 2

del a1

assert count_ref((id(a))) == 1

The garbage collector’s job here is to delete any object stored on the private heap whose reference count becomes 0. For example, in the code above, when the Python Interpreter exits, the reference count of object a becomes 0 and gets garbage collected.

Garbage collector usage is essential to avoid memory leaks. For example, the famous ORM library for handling SQL databases in Python is called SQLALchemy, and it relies heavily on GC to automatically destroy the results of a query. It does this once the query object goes out of scope or when the session object is closed.

5. Testing and debugging in Python

A crucial question that candidates may be asked during testing is something like, "Suppose you are given a feature description to implement. How would you ensure that, upon completion, it functions as intended and meets all the requirements?".

This question tests the candidate's ability to plan, execute and communicate their testing approach and methodology to ensure the delivered product is high quality and meets the customers’ expectations.

To answer this question, it's important to distinguish between two types of requirements: functional and non-functional.

Suppose your task is to write a query that retrieves some rows from a table and then convert the data into a JSON format and send it to a client via HTTPS protocol. The functional requirement is that you write the query and test it to ensure that it retrieves the data correctly. However, the non-functional requirement for this scenario could be the speed of the query execution. If your database contains a large amount of data, the query execution time could be prolonged, leading to slower response times. Therefore, optimizing the query and improving its performance is crucial to meet the non-functional requirements.

To ensure that we have completed the functional requirements of our Python application, we must write tests. We can use different types of testing strategies, such as functional, unit, regression, and integration testing, depending on the nature of the application, the requirements, and the desired outcomes.

Similarly, for testing non-functional requirements, we can use different strategies such as performance, usability, reliability, and security testing. The selection of the appropriate testing strategy will depend on the specific requirements of the application and the goals of the testing process.

Some of the most famous testing libraries for Python are listed below:

  1. pytest: Provides fixtures, parallel testing, parameterization, plugin system, markers, etc.

  2. unittest: Built-in testing framework and less feature-rich than pytest

  3. doctest: This is a built-in testing framework that allows you to include tests in your code's documentation string.

  4. timeit: A built-in module to measure the execution time as a function of a code block.

The last topic we touch upon is debugging. There are different ways to debug a Python program. These are several techniques used for debugging.

  1. Problem simplification: When we isolate the problem by removing any unnecessary code, we may find the root cause of the problem faster and easier.

  2. Use print statements or raise an exception.

  3. pdb: The built-in Python debugger can help walk you through your code line-by-line to find the reason for the problem.

  4. Use IDE features for debugging.

In conclusion, we discussed the essential topics in Python that a seasoned and sophisticated developer must be aware of.

Find your next developer within days, not months

We can help you deliver your product faster with an experienced remote developer. All from €31.90/hour. Only pay if you’re happy with your first week.

In a short 25-minute call, we would like to:

  • Understand your development needs
  • Explain our process to match you with qualified, vetted developers from our network
  • Share next steps to finding the right match, often within less than a week

Not sure where to start?

Let’s have a chat

First developer starts within days. No aggressive sales pitch.