Build a production-ready web application Part 1: Python Basics

Build a production-ready web application Part 1: Python Basics

This article helps the software engineers who have Web development experience in other languages to migrate to Python Web development quickly, it contains several parts like basics, web development part, etc.

Context

I’m a developer who used Golang in the recent 4 years, I’m getting used to Golang and its framework, because of my team’s preference, we chose Python 3 as the new project’s language, I wanted to write Python code as well as possible in a short time. Because the core of Web development is the same no matter what language it is, I just need to migrate my Web knowledge to Python 3.8+.

This is the checklist of all the essential knowledge I sorted out in the last month.

Python Basics

First, let’s recap the most basic Python syntax. (PS: I’ll mix different languages’ syntax when I switch languages very often, the common case is that I wrote for i in items in Golang)

Entrypoint

1
2
if __name__ == '__main__':
print('hello world')

If-else statement

1
2
3
4
5
6
7
a = 1
if a is None:
print('Nothing')
elif a == 1:
print('Bingo')
else:
print('bye')

Common data structures

It contains array, set, dictionary, tuple.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# array
arr = [1,2,3]
if 1 in arr:
print('1 is in the arr')

# set
visited = set()
visited.add(1)
if 1 in visted:
print('1 is in visited')

# dict, it will throw error when the key is not found
word_mapping = dict()
word_mapping['Peace'] = 'Love'
print(word_mapping['Peace']) # Outputs: Love

# defaultdict, it will return default value when the key is not found
from collections import defaultdict
def def_value():
return "Undefined"

word_mapping = defaultdict(def_value)
print(word_mapping['no existing key']) # Outputs: Undefined

# tuple
countries = ("China", "Netherlands", "Spain")
print(countries[0]) # Outputs: China

Iteration

To iterate an array, list, set, dictionary, you can always use for … in , also it allows you to create an iterable object which contains the iterable object returned by __iter__ and the way to fetch next object by __next__ .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# iterate arr
arr = [1, 2, 3]
for k in arr:
print(k)

# iterate dict
word_mapping = {"Peace": "Love", "Love": "Peace"}
for k in word_mapping:
print(word_mapping[k])

# customized iterable objects
class MyLanguages:
def __init__(self):
self.languages = ["Python", "Golang", "Java"]

def __iter__(self):
self.index = 0
return self

def __next__(self):
if self.index >= len(self.languages):
raise StopIteration
res = self.languages[self.index]
self.index += 1
return res


languages = MyLanguages()
for language in languages:
print(language)

Function

It contains traditional function and anonymous function like the other languages.

Traditional function is the one with def keyword, name of function and function body.

1
2
3
# sum_two accepts two int parameters and return the sum of them.
def sum_two(num1: int, num2: int) -> int:
return num1 + num2

We also want variable parameters sometimes, we can add * before the parameter and the parameter will accept multiple parameters, type type of args is tuple.

1
2
3
4
5
6
7
8
# sum_all accepts multiple integer parameters and sum them up.
def sum_all(*args: int) -> int:
sum = 0
for arg in args:
sum += arg
return sum

sum_all(1, 2, 3)

We want to accept variable name parameters, we can add ** before the parameter and the parameter will receive key=value pairs.

1
2
3
4
5
6
7
def print_vars(**kwargs):
for k in kwargs:
print(k, "=>", kwargs[k])

print_vars(name="songrgg", hobby='cooking')
# name => songrgg
# hobby => cooking

Note that the **kwargs parameters should be at the end of the parameter list, otherwise it’s not straightforward for the Python interpreter to parse the parameters afterwards.

For anonymous functions, we use lambda as the keyword, it can accept multiple parameters but only one expression in the body.

1
2
sum_two = lambda num1, num2: num1 + num2
print(sum_two(1, 2)) # Output: 3

It’s super useful when you’re using some map, filter, sort, etc and it accepts a lambda function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# map
numbers1 = [1, 2, 3]
numbers2 = [4, 5, 6]

result = map(lambda x, y: x + y, numbers1, numbers2)
print(list(result)) # Output: [5, 7, 9]

# filter
number_list = range(-5, 5)
less_than_zero = list(filter(lambda x: x < 0, number_list))
print(less_than_zero) # Output: [-5, -4, -3, -2, -1]

# sort based on the 1-index value asc
a = [(1, 2), (4, 1), (9, 10), (13, -3)]
a.sort(key=lambda x: x[1])
print(a) # Output: [(13, -3), (4, 1), (1, 2), (9, 10)]

Class definition

The class definition starts with class keyword, the class contains some functions by convention, like the constructor function __init__ , method call __call__ which allows you to call the class instance like a function.

The self parameter appears for those instance functions, it’s the this keyword in other languages, a pointer to the instance itself, it’s always as the first parameter.

1
2
3
4
5
6
7
8
9
10
11
class Sample:
def __init__(self):
print('Sample init')

def __call__(self):
print('Sample is called implicitly by __call__')

sample = Sample()

# shorthand for sample.__call__()
sample()

There are also class functions whose scope is the class, the first parameter is cls instance, they will be introduced in the decorators section.

Decorators

A decorator is a design pattern in Python that allows a user to add new functionality to an existing object without modifying its structure.

1
2
3
4
5
6
7
8
9
10
11
12
13
def echo_first(f):
""" Nothing special, echo first before the method is called """
def new_f(*args, **kwargs):
print('echo')
f(*args, **kwargs)
return new_f

class Test:
@echo_first
def method(self):
print('method called')

Test().method()

In this sample, it’s a simple function decorator that print echo first before the method is called.

There are some common used decorators we can use, @staticmethod prevents the method access to the class or instance, @classmethod limits the access to the class itself, instance_method has the biggest scope.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test:
@staticmethod
def static_method():
print("I don't have access to the class or instance")

@classmethod
def class_method(cls):
print("I only have access to the class")

def instance_method(self):
self.value = 1
Test2.cls_value = 2
print("I have access to both class and instance")

There is @property decorator to call the setter/getter/deleter of the property in a native way,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class PropertyTest:
@property
def x(self):
"""I am the 'x' property."""
print('getter called')
return self._x

@x.setter
def x(self, value):
print('setter called')
self._x = value

@x.deleter
def x(self):
del self._x

pt = PropertyTest()
pt.x = 34 # setter called
print(pt.x)
# Output: getter called
# 4
del pt.x

Error handling

1
2
3
4
5
6
7
8
9
10
11
12
try: 

except (ValueError, ZeroDivisionError):

else:
# no exceptions raised
finally:
# cleanup code

# Raising exceptions
if x < 1:
raise ValueError("…")

Concurrency

Coroutines are very similar to threads. However, coroutines are cooperatively multitasked, whereas threads are typically preemptively multitasked. Coroutines provide concurrency but not parallelism. The advantages of coroutines over threads are that they may be used in a hard-realtime context (switching between coroutines need not involve any system calls or any blocking calls whatsoever), there is no need for synchronization primitives such as mutexes, semaphores, etc. in order to guard critical sections, and there is no need for support from the operating system.

The Python doc gives several examples of how to define coroutines and execute them,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import asyncio
import time

async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)

async def main():
print(f"started at {time.strftime('%X')}")

await say_after(1, 'hello')
await say_after(2, 'world')

print(f"finished at {time.strftime('%X')}")

asyncio.run(main())
# it will take around 3 seconds for they're running in sequence

They can’t be executed as the normal methods, if you call main() directly, it returns a coroutine object, to execute it you need pass it to asyncio.run.

To run the coroutines concurrently, you can wrap the coroutine to task by asyncio.create_task ,

1
2
3
4
5
6
7
8
async def main():
task1 = asyncio.create_task(say_after(2, "hello"))
task2 = asyncio.create_task(async_method(2, "world"))
await task1
await task2

asyncio.run(main())
# it will take around 2 seconds for they're running concurrently

It will run for around 2 seconds instead of 4 seconds.

Awaitables

We say that an object is an awaitable object if it can be used in an [await](https://docs.python.org/3/reference/expressions.html#await) expression.

  • Coroutines: the async def functions
  • Tasks: used to schedule coroutines concurrently, created by asyncio.create_task
  • Futures: A Future is a special low-level awaitable object that represents an eventual result of an asynchronous operation.

End

Next Artitlce contains the toolkit of setting up the Web application using FastAPI framework, also it will contain the code format, linter, docker image, etc.

Comments

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×