Skip to content

Python

Table of Contents


Type Hinting in Python

Python is dynamically typed, meaning the interpreter determines a variable's type at runtime. However, Type Hinting allows you to explicitly state what data types are expected, improving code readability and enabling better editor support.

Basic Variable Type Hints

To hint a type, use a colon (:) after the variable name. While hints aren't strictly enforced by Python at runtime, they are vital for static analysis tools.

  • Integer: percentage: int = 90
  • Float: temperature: float = 98.6
  • String: text: str = "Hello"
  • Bytes: file_data: bytes = b"..."

Note: Even with a hint, you can still reassign a variable to a different type (e.g., changing a str to an int). To catch these inconsistencies, use a static type checker like MyPy.


Type Hinting in Functions

Type hints are most powerful within functions. They enable the code editor to provide "IntelliSense" (autocompletion) for methods.

Parameters and Return Types

Use a colon for parameters and the -> arrow for the return type.

def get_root(number: int) -> float:
  return number ** 0.5

root_25 = get_root(25) # Editor knows root_25 is a float

Advanced Types: Unions and Optionals

Sometimes a variable can be more than one type.

  • Union (Pipe Operator): Use | to allow multiple types.
  • number: int | float (Can be an integer OR a float)

  • Optional Values: Useful for arguments that might be None.

  • value: str | None = None

Sequence Data Types

For collections like lists and dictionaries, you can hint the types of the elements contained within them.

Type Syntax Example Description
List digits: list[int] A list containing only integers.
Tuple (Fixed) city: tuple[str, float] A tuple with exactly one string and one float.
Tuple (Variable) data: tuple[int, ...] A tuple of integers with an unknown length.
Dictionary vehicle: dict[str, any] Keys are strings; values can be any type.

Union, pipe operator (|) in sequences data types

  • Dictionary values can be of 2 types: dict[str, int | float]
vehicle: dict[str, str | int] = {
  "brand": "Toyota",
  "price": 25000,
}
  • Dictionary values can be of multiple types: dict[str | any]
from typing import Any

vehicle: dict[str, Any] = {
  "brand": "Toyota",
  "price": 25000,
  "weight": 1500,90,
}
  • Sequence elements can be of multiple types: list[int | str]

Custom Classes and the Any Type

You aren't limited to built-in types. You can use your own classes as hints.

class Vehicle:
  def __init__(self, brand: str, type: str):
    self.brand = brand
    self.type = type

# Hinting that a variable should be an instance of Vehicle
car: Vehicle = Vehicle("Toyota", "Sedan")

If you don't care about the type or want to opt-out of type checking for a specific variable, use the Any type from the typing module: from typing import Any


Static Analysis with MyPy

To turn these "hints" into "errors" (during development), install the MyPy extension in your IDE (like VS Code). MyPy will scan your code and flag incompatible types:

  • Example Error: Argument 1 to "get_root" has incompatible type "str"; expected "int"

Decorators and Type Hinting

In Python, decorators are a powerful way to modify or extend the behavior of functions or methods without permanently altering their source code. They are essentially "wrappers" that execute code before and after the target function.


Understanding Decorator Basics

A decorator is a function that takes another function as an argument and returns a new function (the "wrapper").

  • The @ Syntax: Placing @decorator_name above a function definition is shorthand for func = decorator_name(func).
  • The Wrapper: Inside the decorator, we define a nested function that handles the custom logic and calls the original function.
def fence(func):
  def wrapper():
    print("+" * 10)  # Logic before
    func()           # The original function
    print("+" * 10)  # Logic after
  return wrapper

@fence
def log_message():
  print("System Running")

log_message()

Decorators with Arguments

Sometimes you want to pass configuration values to the decorator itself (e.g., choosing which character to use for the "fence"). This requires three levels of nesting:

  1. Outer level: Receives the decorator arguments.
  2. Middle level: Receives the function to be decorated.
  3. Inner level (Wrapper): Executes the logic.
def custom_fence(symbol: str = "+"):
  def decorator(func):
    def wrapper():
      print(symbol * 10)
      func()
      print(symbol * 10)
    return wrapper
  return decorator

@custom_fence(symbol="#")
def log_status():
  print("Active")

Type Hinting for Callables

When writing decorators or functions that accept other functions, you should use the Callable type hint from the typing module.

Syntax for Callable

The syntax follows Callable[[ArgumentTypes], ReturnType].

Scenario Syntax Example Description
Simple Function Callable[[], None] A function that takes no arguments and returns nothing.
Math Function Callable[[int, int], float] Takes two integers and returns a float.
Flexible Function Callable[..., Any] Takes any number of arguments and returns any type.
from typing import Callable, Any

def execute_task(task: Callable[[str], Any], value: str):
  return task(value)

Real-World Example: Simple Routing

Decorators are the backbone of web frameworks like FastAPI. They are used to register functions as "routes" that the server can call when a specific URL is requested.

from typing import Callable, Any

# A registry to store our routes
route_registry: dict[str, Callable[..., Any]] = {}

def route(path: str):
  def decorator(func: Callable[..., Any]):
    # Register the function to the path
    route_registry[path] = func
    return func
  return decorator

@route("/inventory")
def get_inventory():
  return {"items": ["Laptop", "Mouse"]}

# Emulating a server request
request_path = "/inventory"
if request_path in route_registry:
  response = route_registry[request_path]()
  print(f"Response: {response}")
else:
  print("404 Not Found")

Common Pitfalls

  • Forgetting to Return the Wrapper: If your decorator doesn't return the wrapper function, the decorated function will become None, leading to a TypeError: 'NoneType' object is not callable.
  • Missing *args and **kwargs: To make a decorator truly universal (able to wrap functions with any number of arguments), the wrapper should use *args and **kwargs.

Infrastructure Automation with Python

While shell scripting (bash) is excellent for moving files or starting system services, it struggles to handle complex structured data (like JSON or XML) or interact cleanly with modern REST APIs.

This is where Python becomes a critical tool for DevOps and Site Reliability Engineering.

The requests Library

The standard way to interact with web APIs in Python is the third-party requests library. It abstracts away the complexity of HTTP connections, headers, and authentication.

import requests

# Example: Sending a POST request to authenticate with an API
url = "http://api.internal.local/v1/auth"
payload = {"username": "admin", "password": "password123"}

response = requests.post(url, json=payload)

if response.status_code == 200:
    print("Success!")
    data = response.json() # Automatically parses the JSON string into a Python Dictionary
else:
    print(f"Failed: {response.status_code}")

Bridging CLI tools with Python

In advanced automation (like configuring applications inside a Kubernetes cluster), you often need to combine the raw power of a CLI tool (like kubectl) with the logic of Python.

You can use the built-in subprocess module to execute CLI commands, capture their text output, parse it in Python, and then use that data to make an intelligent API request.

import subprocess
import requests

# 1. Reach into a server/container to steal a secret token
cmd = "kubectl exec deploy/myapp -- cat /config/api.token"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
secret_token = result.stdout.strip()

# 2. Use that token to configure another service via REST API
headers = {"X-Api-Key": secret_token}
requests.post("http://other-app.local/api/sync", headers=headers)

This pattern—extracting state via CLI and injecting state via REST API—is the foundation of "Zero-Touch" infrastructure provisioning.