Engineering
Python Primer

Python is extremely useful in almost every area of software development. In embedded engineering Python is most useful for CI/CD automation. In 2023, according to PYPL (PopularitY of Programming Language) python was #1 programming language claiming 28% market share.

I have put together this cheatsheet to help you go from zero to hero in python in the shortest amount of time possible. We will cover a wide range of topics going from the simplest variable declarations, to abstract classes and more complex design patterns.

This cheatsheet is designed to easy to scroll on a single page so that you can quickly find the information you need.

Variables and Data Types

Python is a dynamically typed programming language. You do not need to declare variable types, rather the type of the variable is determined by the value that it contains.

name = "Foo"    # String
age = 30        # Integer
a_number = 5.9  # Float
a_flag = True   # Boolean

Lists

Python is strong with lists. Lists in python are ordered collections that can hold items of different types.

items = ["cat", "dog", "tree"]
items.append("car")  # Add an item
print(items[1])      # Access the second item

Dictionaries

Dictionaries provide the basic means of structured data representation in python. Dictionaries store key-value pairs.

person = {"name": "John", "age": 30}
person["height"] = 5.9  # Add a new key-value pair
print(person["name"])   # Access value by key

Deques

from collections import deque
 
queue = deque(["Eric", "John", "Michael"])
queue.append("Terry")           # Terry arrives
queue.popleft()                 # The first to arrive now leaves
 
print(queue)                    # Remaining queue in order of arrival

Enums

from enum import Enum, auto
 
class Color(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()
 
print(Color.RED)

Data Classes

Data classes give you a simple way to create structured objects that contain data. They are easier to access than dict types since you can use the dot . notation.

from dataclasses import dataclass
 
@dataclass
class Product:
    name: str
    price: float
 
product = Product("Widget", 19.99)
print(product.name)

Control Structures

If Statements

if age > 18:
    print("Adult")
else:
    print("Not Adult")

You can also write it like this:

variable = "Adult" if age > 18 else "Not Adult"

For Loops

for item in item_list:
    print(fruit)

Range loops:

for i in range(0, 10):
    print(i)

You can also just loop without using the value. In that case use underscore _ to denote placeholder variables:

for _ in range(0, 10):
    pass

While Loops

while len(items):
    items.pop()  # pops last item and removes it from the list

Functions

def length(name: str) -> int:
    return len(str)
print(length("foo"))

You can use type hints as I did above in order to be able to use static analysis tools like mypy to help you find bugs in your program. In general you should always use type hints with all variables.

Classes

Object-oriented programming is easy in python.

class Person:
    def __init__(self, name: str, age: int):
        self._name = name
        self._age = age
 
    def greet(self):
        print(f"Hello, my name is {self._name} and I am {self._age} years old.")
 
person = Person("John", 30)
person.greet()

Use underscore _ prefix to designate "private" properties which should not be accessed directly. Python will allow you to access these properties anyway but static analysis checks will warn you about such unintended access.

Abstract classes

from abc import ABC, abstractmethod
 
class AbstractClass(ABC):
    @abstractmethod
    def my_method(self):
        pass
 
class ConcreteClass(AbstractClass):
    def my_method(self):
        print("Implementing the abstract method")
 
obj = ConcreteClass()
obj.my_method()

Meta classes

Metaclasses are the 'classes of classes', allowing you to control the creation of classes.

class Meta(type):
    def __new__(cls, name, bases, dct):
        # Custom processing before class creation
        return super().__new__(cls, name, bases, dct)
 
class MyClass(metaclass=Meta):
    pass

Dynamic attributes

You can use __getattr__, __setattr__, and @property for dynamic attribute management.

class MyClass:
    def __init__(self, value):
        self._value = value
 
    def __getattr__(self, name):
        # Custom behavior for undefined attributes
        if name == "dynamic_attribute":
            return "Dynamic Value"
        raise AttributeError(f"{name} not found")
 
    @property
    def value(self):
        return self._value
 
    @value.setter
    def value(self, value):
        self._value = value
 
obj = MyClass(10)
print(obj.dynamic_attribute)  # Dynamic Value
obj.value = 20
print(obj.value)

List Comprehensions

List comprehensions provide a concise way to process lists. They allow you to easily loop through items and modify them returning a new list as result. All in one line of Python code.

squares = [x**2 for x in range(10)]
print(squares)

Dictionary Comprehensions

Similar to list comprehensions, but for dictionaries.

Example:

square_dict = {x: x**2 for x in range(5)}
print(square_dict)
# result:
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

Lambda Functions

You can define anonymous functions and pass them as variables using lambda keyword.

Example:

times_two = lambda x: x * 2
print(times_two(5))

Map and Filter

map and filter are functions that apply a function to each item in a list. You can use lambda functions and pass them directly as parameter to map or filter.

nums = [1, 2, 3, 4]
squared = list(map(lambda x: x**2, nums))
print(squared)
even_nums = list(filter(lambda x: x % 2 == 0, nums))
print(even_nums)

You can also filter like this:

[x for x in [1,2,3,4] if x < 3]
# result:
[1, 2]

Exception Handling

Use try and except blocks to handle exceptions.

try:
    result = 42 / 0
except ZeroDivisionError:
    print("Division by zero!")

Pass exception further:

try:
    raise ValueError("Initial error")
except ValueError as e:
    raise RuntimeError("Handling the error") from e

YAML

Yaml is extremely powerful text based structured data description language that supports data descriptions from basic objects to complex hierarchies with inheritance.

import yaml
document = """
  a: 1
  b:
    c: 3
    d: 4
"""
print yaml.dump(yaml.load(document))

JSON

JSON is also very easy to parse in python. Although not as flexible and powerful as yaml.

import json
 
# Converting from JSON
user_json = '{"name": "John", "age": 30}'
user = json.loads(user_json)
print(user['name'])
 
# Converting to JSON
user_dict = {"name": "John", "age": 30}
user_json = json.dumps(user_dict)
print(user_json)

Virtual Environments

Starting from recent 2023 ubuntu release, all python package installation should be done through virtual environments.

Creating a virtual environment:

python3 -m venv my_venv

Activating a virtual environment:

  • source my_venv/bin/activate

You can create a default virtual environment in your home directory and then activate it by default in every terminal by placing the activation code above into your .bashrc.

Modules and Packages

Any python file is treated as a module, meaning you can import classes and functions from it using the import statement.

import math
from .my_local_module import func
print(math.sqrt(16))
func()

A package in python is a collection of modules. It is defined by creating a special __init__.py file in the same directory as your modules. In this file you can then import modules that you expect to also be exported from your package.

Decorators

Decorators in python allow you to enhance your functions without modifying their code.

def my_decorator(func):
    def wrapper():
        print("This is run before the function is called.")
        func()
        print("This is run after the function is called.")
    return wrapper
 
@my_decorator
def say_hello():
    print("Hello!")
 
say_hello()

Decorators can also return a dynamic functions which allows you to add additional logic in your decorators and even instantiate the same decorator multiple times:

def repeat(times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat
 
@repeat(times=3)
def greet(name):
    print(f"Hello {name}")
 
greet("Alice")

Decorators with arguments:

def decorator_with_args(arg1, arg2):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"Arguments passed to decorator: {arg1}, {arg2}")
            return func(*args, **kwargs)
        return wrapper
    return decorator
 
@decorator_with_args('hello', 'world')
def my_function():
    print("Function execution")
 
my_function()

Generators

Generators allow you to create iterators in a more straightforward and memory-efficient way.

def countdown(num):
    while num > 0:
        yield num
        num -= 1
 
for i in countdown(5):
    print(i)

Thread Concurrency

from threading import Thread
 
def print_numbers():
    for i in range(5):
        print(i)
 
def print_letters():
    for letter in ['a', 'b', 'c', 'd', 'e']:
        print(letter)
 
thread1 = Thread(target=print_numbers)
thread2 = Thread(target=print_letters)
 
thread1.start()
thread2.start()
 
thread1.join()
thread2.join()

Process Concurrency

Forking processes in python is easy.

from multiprocessing import Process
 
def task():
    print("This is a multiprocessing task.")
 
process = Process(target=task)
process.start()
process.join()

Regular expressions

Regular expressions is way of describing patterns that can directly parse a class of input data called "regular languages" in automata theory.

In python you can use these expressions to match complex patterns in strings.

import re
 
pattern = re.compile(r'\b[A-Za-z]+\b')
sentence = "Regex is fun!"
matches = pattern.findall(sentence)
print(matches)

Asynchronous IO

In contrast to threads which implement preemptive concurrency (through operating system functions), asynchronous IO interfaces with kpoll, select and epoll family of calls internally. This allows your code to "sleep" while it is waiting for asynchronous events being delivered from multiple sources. This is why async always involves a main loop that sleeps most of the time and only wakes up and runs your code when events occur.

import asyncio
 
async def main():
    print('Hello')
    await asyncio.sleep(1)
    print('World')
 
asyncio.run(main())

Scatter gather:

import asyncio
 
async def task(name, delay):
    await asyncio.sleep(delay)
    print(f"Task {name} finished")
 
async def main():
    tasks = [
        task("A", 1),
        task("B", 2),
        task("C", 3)
    ]
    await asyncio.gather(*tasks)
 
asyncio.run(main())

Asynchronous IO is perfect for situations where the problem is best solved with monitoring a large number of unix file descriptors (this can be sockets, files, eventfd descriptors etc) for events while sleeping most of the time. You have one thread, but can monitor multiple event sources.

Unit testing

Python provides very powerful unit testing framework that you can use to test your code (have a look at pytest).

import unittest
 
def add(a, b):
    return a + b
 
class TestAddition(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(1, 2), 3)

Notice that in this case we have tests directly embedded in the file. While you can do this (you can run the above code using pytest file.py), normally you would place your test cases into tests folder and prefix files containing test cases with test_ prefix.

File Handling

Reading and writing files is straightforward in python.

with open('output.txt', 'w') as f:
    f.write("Hello, world!")
with open('input.txt', 'r') as f:
    data = f.read()
    print(data)

Context Managers

This is how you implement objects that can be used in a with block.

class MyContextManager:
    def __enter__(self):
        print("Enter the context!")
        return self
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Exit the context!")
 
with MyContextManager() as manager:
    print("Inside the context!")

Debugging

You can debug python directly using debugging helpers.

import pdb
 
def divide(a, b):
    pdb.set_trace()
    return a / b
 
print(divide(1, 0))

Run using python -m pdb my_script.py. Even if you run your script using python3 my_script.py the script will pause at the line before the return and you will be able to inspect your variables.

Profiling

You can use cProfile to profile your code in python:

import cProfile
import re
 
def regex_operations():
    return re.compile("foo|bar").match("bar")
 
cProfile.run('regex_operations()')

Design patterns: Singleton

class Singleton:
    _instance = None
 
    def __new__(self):
        if self._instance is None:
            self._instance = super(Singleton, self).__new__(self)
        return self._instance
 
singleton1 = Singleton()
singleton2 = Singleton()
print(singleton1 is singleton2)  # True

Packaging

You should always package and version your python code into python packages that can be installed with pip. This ensures orderly releases and easy version management.

When you crate a pyproject.toml file in the root of your project you can then build a pip package using python -m build command.

Useful Libraries

  • NumPy: numerical analysis and AI programming.
import numpy as np
a = np.array([1, 2, 3])
print(a + 1)
  • Pandas: useful for data manipulation and analysis.
import pandas as pd
df = pd.DataFrame({'Name': ['John', 'Anna'], 'Age': [28, 24]})
print(df)
  • Matplotlib: for plotting graphs.
import matplotlib.pyplot as plt
plt.plot([1, 2, 3], [5, 7, 4])
plt.show()
  • SQLite: for simple database
import sqlite3

connection = sqlite3.connect('example.db')
cursor = connection.cursor()

# Create table
cursor.execute('''CREATE TABLE IF NOT EXISTS stocks
                  (date text, trans text, symbol text, qty real, price real)''')

# Insert a row of data
cursor.execute("INSERT INTO stocks VALUES ('2006-01-05','BUY','RHAT',100,35.14)")

# Save (commit) the changes
connection.commit()

# Close the connection when done
connection.close()
  • Flask: easy creation of web endpoints.

Flask is a very powerful library for creating network endpoints and apis:

from flask import Flask
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    return 'Hello, World!'
 
if __name__ == '__main__':
    app.run()

For outgoing requests you can use requests library:

import requests
 
response = requests.get('https://api.github.com')
data = response.json()
 
print(data)
  • TKinter: quick graphical interfaces
import tkinter as tk
 
root = tk.Tk()
root.title("Simple GUI")
root.geometry("300x200")
 
label = tk.Label(root, text="Hello, Tkinter!")
label.pack()
 
root.mainloop()
  • nltk: natural language processing
import nltk
nltk.download('punkt')
from nltk.tokenize import word_tokenize
 
text = "Hello there, how are you?"
tokens = word_tokenize(text)
print(tokens)
# Result:
['Hello', 'there', ',', 'how', 'are', 'you', '?']
  • Tensorflow: machine learning and model identification
import tensorflow as tf
 
# Define a Sequential model with a single dense layer
model = tf.keras.models.Sequential([
  tf.keras.layers.Dense(units=1, input_shape=[1])
])
 
model.compile(optimizer='sgd', loss='mean_squared_error')
 
# Training data: y = 2x - 1
xs = [-1.0, 0.0, 1.0, 2.0, 3.0, 4.0]
ys = [-3.0, -1.0, 1.0, 3.0, 5.0, 7.0]
 
# Train the model
model.fit(xs, ys, epochs=500, verbose=0)
 
# Predict the value of y for x = 10.0
print(model.predict([10.0]))
# Result:
[[18.98]]
  • PuLP: solving linear programming problems. This allows you to define constraints and then solve the problem while adhering to all specified constraints.
from pulp import *
 
# Define the problem
prob = LpProblem("Simple_Problem", LpMaximize)
 
# Define the variables
x = LpVariable("x", 0, None)  # x >= 0
y = LpVariable("y", 0, None)  # y >= 0
 
# Objective function
prob += 3*x + 2*y, "Objective"
 
# Constraints
prob += x <= 20, "Constraint_x"
prob += y <= 30, "Constraint_y"
prob += x + y == 30, "Constraint_sum"
 
# Solve the problem
prob.solve()
 
# Print the results
print(f"Status: {LpStatus[prob.status]}")
for v in prob.variables():
    print(f"{v.name} = {v.varValue}")
# Result:
x = 20.0
y = 10.0
  • Streamlit: a faster way to build web application.
import streamlit as st
 
# Write a title and some text to the app:
st.title('Hello, Streamlit!')
st.write('This is a simple Streamlit application.')

To run do: streamlit run file.py

The possibilities are endless.

Martin SchröderMartin Schröder
16 years  of experience

About the author

Martin is a full-stack expert in embedded systems, data science, firmware development, TDD, BDD, and DevOps. Martin serves as owner and co-founder of Swedish Embedded Consulting.

Expertise

Embedded Firmware
Zephyr RTOS
Scrum
Continuous Delivery

Contact Martin

By completing signup, you are agreeing to Swedish Embedded's Terms of Service and Privacy Policy and that Swedish Embedded may use the supplied information to contact you until you choose to opt out.

Confirm your contact information

Thank you!

An email has been sent to you with a link to book a short call where we can discuss your project further. If you have any further questions, please send an email to info@swedishembedded.com