You don't need to learn every corner of Python before being productive. This lesson covers the data types and control structures you'll touch every day in DevOps work.
Primitive Types
name = "alex" # str
count = 42 # int
ratio = 3.14 # float
active = True # bool
nothing = None # NoneType
type(count) # <class 'int'>
Python is dynamically typed — variables don't declare a type, but they always have one. You can add optional type hints:
def greet(name: str) -> str:
return f"hello, {name}"
Type hints aren't enforced at runtime; they're for tools like mypy/pyright and your editor. Use them in anything beyond a throwaway script.
Strings and f-strings
user = "alex"
count = 7
print(f"user {user} has {count} items")
print(f"upper: {user.upper()}, len: {len(user)}")
print(f"{count:04d}") # 0007 — zero-padded
print(f"{3.14159:.2f}") # 3.14 — two decimals
F-strings (Python 3.6+) are the modern way to format. Avoid the older % and .format() styles in new code.
Collections
| Type | Syntax | Properties |
|---|---|---|
| list | [1, 2, 3] | ordered, mutable, allows duplicates |
| tuple | (1, 2, 3) | ordered, immutable, allows duplicates |
| set | {1, 2, 3} | unordered, mutable, no duplicates |
| dict | {"a": 1} | key-value, ordered (insertion), mutable |
servers = ["web-1", "web-2", "db-1"]
servers.append("cache-1")
servers[0] # 'web-1'
servers[-1] # 'cache-1'
servers[1:3] # ['web-2', 'db-1']
config = {"region": "us-east-1", "instance_type": "t3.micro"}
config["region"]
config.get("missing", "default")
for key, value in config.items():
print(key, value)
unique_ips = set(["1.1.1.1", "1.1.1.1", "8.8.8.8"]) # 2 elements
Conditionals
status_code = 503
if status_code < 300:
print("ok")
elif status_code < 400:
print("redirect")
elif status_code < 500:
print("client error")
else:
print("server error")
Truthiness: empty collections, 0, empty string, and None are all falsy. So if items: is the idiomatic way to test "do I have any items".
Loops
for server in servers:
print(server)
for i, server in enumerate(servers):
print(f"{i}: {server}")
for region, az_count in [("us-east-1", 3), ("eu-west-1", 2)]:
print(region, az_count)
count = 5
while count > 0:
print(count)
count -= 1
break exits a loop; continue skips to the next iteration; else on a loop runs only if the loop didn't break.
Comprehensions
The Pythonic way to build a new collection from an existing one:
numbers = [1, 2, 3, 4, 5]
squares = [n * n for n in numbers]
evens = [n for n in numbers if n % 2 == 0]
names = ["Alex", "Sam", "Jamie"]
upper_map = {name: name.upper() for name in names}
unique_first_letters = {name[0] for name in names}
Use comprehensions for clarity, not contortion. If yours needs more than one for or a complex filter, write a normal loop.
Functions
def fetch(url: str, timeout: int = 30, *, retries: int = 3) -> dict:
"""Fetch a URL with retries. Keyword-only retries."""
...
# Positional + keyword
fetch("https://example.com", 10, retries=5)
# All keyword
fetch(url="https://example.com", timeout=10)
Notes:
- Arguments after a bare
*are keyword-only — callers must name them. Improves readability of boolean flags. - Default values are evaluated once at definition time. Never use mutable defaults like
def foo(items=[]); useNoneand assign inside the body. *argscollects extra positionals into a tuple;**kwargscollects extra keyword args into a dict.
Pattern Matching (3.10+)
Cleaner branching on the structure of data:
def describe(event: dict) -> str:
match event:
case {"type": "create", "resource": name}:
return f"creating {name}"
case {"type": "delete", "resource": name}:
return f"deleting {name}"
case {"type": kind}:
return f"unknown event: {kind}"
case _:
return "malformed event"
Truthy Patterns You'll See Constantly
# Default if missing or falsy
region = config.get("region") or "us-east-1"
# Walrus operator (3.8+) — assign and test in one go
if (n := len(items)) > 100:
print(f"large batch of {n}")
# Unpacking
first, *rest = [1, 2, 3, 4]
a, b = b, a # swap
You don't need to memorise every detail of these. Build mental shortcuts as you encounter them in real code.