# Python Basics & CLI Cheatsheet

- URL: https://karandeepsingh.ca/cheatsheets/python/python-basics-cli/
- Author: Karandeep Singh
- Published: 2025-05-12
- Tags: python, cli
- Summary: Notes on core Python syntax, error handling, file I/O with pathlib, plus the CLI side (venv, pip, argparse, and python -m tricks).

Python shows up in every DevOps job I've had, from quick log parsers to full AWS automation. These are the syntax bits and CLI commands I look up most often. For AWS scripting specifically, see my [Python Boto3 Cheatsheet](https://karandeepsingh.ca/cheatsheets/python/python-boto3/).

## Variables & Types

> **Docs:** [Built-in Types, Python 3 docs](https://docs.python.org/3/library/stdtypes.html)

```python
name = "karandeep"          # str
count = 42                  # int
ratio = 0.75                # float
enabled = True              # bool
nothing = None              # NoneType

# Multiple assignment and swap
a, b = 1, 2
a, b = b, a

# Type checks and conversion
isinstance(count, int)      # True
int("42"), str(42), float("0.5"), bool("")   # 42, '42', 0.5, False
```

## Strings & f-strings

> **Docs:** [String methods, Python 3 docs](https://docs.python.org/3/library/stdtypes.html#string-methods)

```python
s = "prod-web-01"
s.upper(), s.lower(), s.title()
s.startswith("prod"), s.endswith("01")
s.split("-")                # ['prod', 'web', '01']
"-".join(["prod", "db", "02"])
s.replace("web", "app")
s.strip(), s.lstrip(), s.rstrip()
"web" in s                  # True

# f-strings (use these, always)
env, cpu = "prod", 87.5432
f"{env=}"                   # "env='prod'" - great for debugging
f"{cpu:.2f}%"               # "87.54%"
f"{count:>8}"               # right-align, width 8
f"{1024**3:,}"              # "1,073,741,824"
f"{0.875:.1%}"              # "87.5%"
```

## Lists, Dicts, Sets & Tuples

> **Docs:** [Data Structures in the Python tutorial](https://docs.python.org/3/tutorial/datastructures.html)

```python
# list - ordered, mutable
hosts = ["web1", "web2"]
hosts.append("web3"); hosts.extend(["db1"]); hosts.insert(0, "lb1")
hosts.remove("web1"); last = hosts.pop()
hosts[0], hosts[-1], hosts[1:3], hosts[::-1]     # index, last, slice, reverse
sorted(hosts), len(hosts), "db1" in hosts

# tuple - ordered, immutable (good dict keys)
point = ("us-east-1", "a")

# dict - key/value
cfg = {"env": "prod", "replicas": 3}
cfg["env"]; cfg.get("region", "us-east-1")       # .get avoids KeyError
cfg["region"] = "eu-west-1"
cfg.keys(), cfg.values(), cfg.items()
merged = cfg | {"replicas": 5}                    # 3.9+ merge operator
cfg.setdefault("tags", []).append("blue")

# set - unique, unordered
seen = {"web1", "web2"}
seen.add("web3"); seen.discard("web1")
{"a", "b"} & {"b", "c"}     # intersection {'b'}
{"a", "b"} | {"b", "c"}     # union
{"a", "b"} - {"b"}          # difference
```

## Comprehensions

> **Docs:** [List comprehensions, Python 3 tutorial](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions)

```python
squares = [x * x for x in range(10)]
evens   = [x for x in range(10) if x % 2 == 0]
by_name = {h: len(h) for h in hosts}                 # dict comprehension
unique  = {line.split()[0] for line in log_lines}    # set comprehension
gen     = (x * x for x in range(10**9))              # generator - lazy, no memory blowup
flat    = [x for row in matrix for x in row]         # nested flatten
```

## Functions

> **Docs:** [Defining functions, Python 3 tutorial](https://docs.python.org/3/tutorial/controlflow.html#defining-functions)

```python
def deploy(service: str, replicas: int = 1, *, dry_run: bool = False) -> str:
    """Args after * are keyword-only."""
    return f"deploy {service} x{replicas} dry_run={dry_run}"

deploy("api", replicas=3, dry_run=True)

def merge_tags(*args, **kwargs): ...     # variadic positional / keyword
run = lambda cmd: cmd.split()            # small throwaway functions

# Never use a mutable default
def add_host(host, hosts=None):
    hosts = hosts if hosts is not None else []
```

## Classes (briefly)

> **Docs:** [Python dataclasses docs](https://docs.python.org/3/library/dataclasses.html)

```python
from dataclasses import dataclass, field

@dataclass
class Server:
    name: str
    env: str = "dev"
    tags: list[str] = field(default_factory=list)

    def fqdn(self) -> str:
        return f"{self.name}.{self.env}.example.com"

s = Server("web1", env="prod")
s.fqdn()                     # 'web1.prod.example.com'
```

## Error Handling

> **Docs:** [Errors and Exceptions, Python 3 tutorial](https://docs.python.org/3/tutorial/errors.html)

```python
try:
    result = 10 / int(value)
except (ValueError, ZeroDivisionError) as e:
    print(f"bad input: {e}")
except Exception:
    raise                    # re-raise unexpected errors - don't swallow them
else:
    print("only runs if no exception")
finally:
    cleanup()                # always runs

# Raise your own, chain the cause
raise RuntimeError("deploy failed") from e

# Context managers handle cleanup for you
with open("app.log") as f, open("out.log", "w") as out:
    out.write(f.read())
```

## File I/O & pathlib

> **Docs:** [pathlib on docs.python.org](https://docs.python.org/3/library/pathlib.html)

```python
from pathlib import Path
import json

p = Path("/etc/app/config.json")
p.exists(), p.is_file(), p.is_dir()
p.name, p.stem, p.suffix, p.parent       # config.json, config, .json, /etc/app
p.read_text(), p.write_text("data")
p.read_bytes()

cfg = json.loads(Path("config.json").read_text())
Path("out.json").write_text(json.dumps(cfg, indent=2))

# Build paths with / - no os.path.join
log_dir = Path.home() / "logs" / "app"
log_dir.mkdir(parents=True, exist_ok=True)

# Globbing
for f in Path("/var/log").glob("*.log"): ...
for f in Path(".").rglob("**/*.yaml"): ...       # recursive

# Stream large files line by line
with open("huge.log") as f:
    for line in f:
        if "ERROR" in line:
            print(line, end="")
```

## Dates & Times

> **Docs:** [Python datetime docs](https://docs.python.org/3/library/datetime.html)

```python
from datetime import datetime, timedelta, timezone, date

now_utc = datetime.now(timezone.utc)          # always prefer aware datetimes
today   = date.today()

# Format / parse
now_utc.strftime("%Y-%m-%d %H:%M:%S")         # '2025-05-12 09:15:00'
now_utc.isoformat()                           # '2025-05-12T09:15:00+00:00'
datetime.strptime("2025-05-12", "%Y-%m-%d")
datetime.fromisoformat("2025-05-12T09:15:00+00:00")

# Arithmetic
deadline = now_utc + timedelta(days=7, hours=3)
age = now_utc - datetime(2022, 7, 23, tzinfo=timezone.utc)
age.days, age.total_seconds()

# Epoch conversions (log timestamps, APIs)
now_utc.timestamp()                           # datetime -> epoch float
datetime.fromtimestamp(1752570000, tz=timezone.utc)

# Named zones without third-party libs (3.9+)
from zoneinfo import ZoneInfo
datetime.now(ZoneInfo("America/Edmonton"))
```

## Sorting

> **Docs:** [Sorting Techniques, Python 3 HOWTO](https://docs.python.org/3/howto/sorting.html)

```python
nums = [3, 1, 2]
sorted(nums)                        # new list; nums.sort() sorts in place
sorted(nums, reverse=True)

# Sort by a key
hosts = ["web10", "web2", "db1"]
sorted(hosts, key=len)
sorted(hosts, key=str.lower)

# Sort dicts / tuples / objects
servers = [{"name": "web1", "cpu": 80}, {"name": "db1", "cpu": 45}]
sorted(servers, key=lambda s: s["cpu"], reverse=True)

from operator import itemgetter, attrgetter
sorted(servers, key=itemgetter("cpu"))
sorted(instances, key=attrgetter("launch_time"))

# Multi-key: env ascending, then cpu descending
sorted(servers, key=lambda s: (s["env"], -s["cpu"]))

# Top/bottom N without a full sort
import heapq
heapq.nlargest(3, servers, key=itemgetter("cpu"))

# Keep a list sorted while inserting
import bisect
bisect.insort(nums, 2)
```

## Queues, Stacks & Heaps

> **Docs:** [collections.deque, Python 3 docs](https://docs.python.org/3/library/collections.html#collections.deque)

```python
# deque - O(1) at both ends: stack AND queue
from collections import deque
q = deque(["job1", "job2"])
q.append("job3")           # enqueue right
q.popleft()                # dequeue left  -> FIFO queue
q.pop()                    # pop right     -> LIFO stack
tail = deque(open("app.log"), maxlen=10)   # like tail -n 10

# heapq - priority queue on a plain list
import heapq
pq = []
heapq.heappush(pq, (2, "deploy"))
heapq.heappush(pq, (1, "hotfix"))
heapq.heappop(pq)          # (1, 'hotfix') - smallest priority first

# queue.Queue - thread-safe, for producer/consumer pipelines
from queue import Queue
jobs = Queue()
jobs.put("build"); jobs.get(); jobs.task_done()
```

The thread-safe `queue.Queue` matters most once worker pools are involved. Full producer/consumer patterns are in the [Python Threading & Concurrency Cheatsheet](https://karandeepsingh.ca/cheatsheets/python/python-threading-concurrency/).

## URLs & HTTP

> **Docs:** [urllib.request on docs.python.org](https://docs.python.org/3/library/urllib.request.html)

```python
# Parse and build URLs
from urllib.parse import urlparse, urlencode, quote, parse_qs
u = urlparse("https://api.example.com/v1/deploy?env=prod&tag=v2")
u.scheme, u.netloc, u.path       # 'https', 'api.example.com', '/v1/deploy'
parse_qs(u.query)                # {'env': ['prod'], 'tag': ['v2']}
urlencode({"env": "prod", "app": "my api"})   # 'env=prod&app=my+api'
quote("path with spaces")                     # 'path%20with%20spaces'

# GET JSON - stdlib only, no dependencies
import json, urllib.request
with urllib.request.urlopen("https://api.github.com/zen", timeout=10) as r:
    body = r.read().decode()
    r.status, r.headers["Content-Type"]

# POST JSON
req = urllib.request.Request(
    "https://api.example.com/v1/deploy",
    data=json.dumps({"env": "prod"}).encode(),
    headers={"Content-Type": "application/json", "Authorization": "Bearer TOKEN"},
    method="POST",
)
with urllib.request.urlopen(req, timeout=10) as r:
    result = json.load(r)
```

For anything beyond a quick script, just `pip install requests` and get sessions, retries, and connection pooling with far less ceremony.

## CLI: Interpreter Flags

> **Docs:** [Command line and environment, Python 3 docs](https://docs.python.org/3/using/cmdline.html)

| Command | What it does |
|---------|--------------|
| `python -c "print(1+1)"` | Run a one-liner |
| `python -i script.py` | Run script, then drop into REPL with its state |
| `python -m module` | Run a module as a script (uses your venv's version) |
| `python -u script.py` | Unbuffered stdout, needed in containers/CI logs |
| `python -X dev script.py` | Dev mode: extra warnings, asyncio debug |
| `python -V` / `python -VV` | Version / verbose version |
| `python -q` | REPL without the banner |

## CLI: Useful `-m` Modules

> **Docs:** [The -m option, Python 3 docs](https://docs.python.org/3/using/cmdline.html#cmdoption-m)

```bash
python -m http.server 8000              # instant file server in cwd
python -m json.tool < data.json         # pretty-print / validate JSON
python -m venv .venv                    # create virtualenv
python -m pip install requests          # pip tied to *this* interpreter
python -m site                          # show sys.path and site-packages
python -m timeit "'-'.join(map(str, range(100)))"
python -m calendar 2026
python -m zipfile -c out.zip src/       # create a zip
python -m tarfile -e release.tar.gz     # extract a tarball
python -m unittest discover             # run tests
python -m pdb script.py                 # step-through debugger
```

## venv & pip

> **Docs:** [Python venv docs](https://docs.python.org/3/library/venv.html)

```bash
python -m venv .venv
source .venv/bin/activate               # Linux/macOS
.venv\Scripts\activate                  # Windows
deactivate

pip install requests boto3              # install
pip install -r requirements.txt         # from file
pip install -e .                        # editable local package
pip install 'urllib3<2'                 # version constraint
pip list --outdated
pip show requests                       # version, deps, location
pip freeze > requirements.txt           # pin exact versions
pip uninstall -y requests
```

**tip:**
Always use `python -m pip` instead of bare `pip`. It guarantees you're installing into the interpreter you think you are, which saves you from the classic "installed it but import fails" mystery on hosts with multiple Pythons.

## argparse Skeleton

The reason to use `argparse` rather than reading `sys.argv` is everything it
gives you for free — a help page, type coercion, and a non-zero exit on bad
input, which is what CI needs:

```text
$ python3 deploy.py --help
usage: deploy.py [-h] [--env {sandbox,live}] [--replicas REPLICAS] [--dry-run]
                 target

Deploy a service to an environment.

positional arguments:
  target                service to deploy

options:
  -h, --help            show this help message and exit
  --env {sandbox,live}  target environment
  --replicas REPLICAS   how many
  --dry-run             print the plan and stop
$ python3 deploy.py web --env live --replicas 3
web -> live x3
```

```text
$ python3 deploy.py web --env prod
# exits with status 2
deploy.py: error: argument --env: invalid choice: 'prod' (choose from 'sandbox', 'live')
# Exit 2, before a single line of your code ran. A typo in a pipeline
# stops there instead of deploying to somewhere that does not exist.
```

> **Docs:** [Python argparse docs](https://docs.python.org/3/library/argparse.html)

```python
#!/usr/bin/env python3
import argparse

def main() -> None:
    parser = argparse.ArgumentParser(description="Deploy helper")
    parser.add_argument("service", help="service name")
    parser.add_argument("-e", "--env", default="dev", choices=["dev", "staging", "prod"])
    parser.add_argument("-r", "--replicas", type=int, default=1)
    parser.add_argument("--dry-run", action="store_true")
    parser.add_argument("-v", "--verbose", action="count", default=0)
    args = parser.parse_args()

    print(f"deploying {args.service} to {args.env} x{args.replicas}")

if __name__ == "__main__":
    main()
```

```bash
./deploy.py api -e prod -r 3 --dry-run
./deploy.py --help                      # auto-generated help
```

## Scripting Essentials

> **Docs:** [Python subprocess docs](https://docs.python.org/3/library/subprocess.html)

```python
import sys, os, subprocess

sys.argv                                 # raw CLI args
sys.exit(1)                              # non-zero = failure, scripts should use it
os.environ.get("AWS_REGION", "us-east-1")

# Run shell commands - capture output, raise on failure
result = subprocess.run(
    ["git", "rev-parse", "--short", "HEAD"],
    capture_output=True, text=True, check=True,
)
sha = result.stdout.strip()
```

When a script outgrows a single loop and needs parallel downloads or API calls, jump to the [Python Threading & Concurrency Cheatsheet](https://karandeepsingh.ca/cheatsheets/python/python-threading-concurrency/).

**Question:** How many of your scripts take their configuration from positional `sys.argv` indexes? What happens to them when someone passes the arguments in the other order?

## Related Cheatsheets

- [Python Threading & Concurrency](https://karandeepsingh.ca/cheatsheets/python/python-threading-concurrency/): Thread, Lock, ThreadPoolExecutor, and asyncio, with a decision table for I/O-bound vs CPU-bound work
- [Python Boto3](https://karandeepsingh.ca/cheatsheets/python/python-boto3/): S3, EC2, Lambda, and DynamoDB automation built on this page's syntax (sessions, paginators, waiters)
- [Bash Basics](https://karandeepsingh.ca/cheatsheets/bash/bash-basics/): the other scripting language on every box, with parameter expansion, tests, traps, and arrays

The rest of my Python notes live in the [Python group](https://karandeepsingh.ca/cheatsheets/python/), and everything else is in the [cheatsheet library](https://karandeepsingh.ca/cheatsheets/).
