Python Variables, Data Types, and Safe Conversion
I worked through Python's core data types, dynamic typing model, and safe conversion patterns using isinstance and type hints. Defensive type handling made automation scripts robust against unexpected input rather than crashing on a bad cast.
Objective & Context
Python infers types at runtime, so input validation must be explicit. This lab covers int, float, str, bool, and None, then builds safe conversion helpers that validate before casting, the foundation for reliable user and file input later.
Learning Objectives
- Inspect runtime types with type() and isinstance() instead of guessing from values.
- Apply type hints to function signatures and understand what they do and do not enforce.
- Write safe conversion helpers that return a default instead of raising on bad input.
- Distinguish mutable from immutable types and predict aliasing behaviour.
- Choose the right numeric, text, and boolean types for automation inputs.
Environment & Prerequisites
Prerequisites
Python 3.11inside an activated venv (see the Python Fundamentals lab)- A REPL (
python) or a scratch script file for experimentation - Optional:
mypyfor static type checking (pip install mypy) - Assumed skill: running Python one-liners and reading tracebacks
Step-by-Step Execution
1. Inspect types at runtime
python -c "x=42; print(type(x), isinstance(x,int))"Expected output:
<class 'int'> True
What this teaches: type() reports the exact runtime class while isinstance() answers the question you usually mean: can this value be used as an int here, including subclasses.
2. Safe conversion with a guard
cat > conv.py <<'EOF'
def to_int(s: str, default: int = 0) -> int:
"""Convert s to int, returning default on any bad input."""
try:
return int(s)
except (ValueError, TypeError):
return default
EOFExpected output:
(no output; conv.py written with the to_int helper)
What this teaches: The guard pattern centralizes the try/except in one helper so call sites stay readable and every conversion fails the same safe way. Saving it to conv.py now makes step 3's import work, since python -c imports modules from the current directory.
3. Verify behaviour on good and bad input
python -c "from conv import to_int; print(to_int('7'), to_int('x'))"Expected output:
7 0
What this teaches: Good input converts normally and bad input returns the default. Testing both paths is what makes the helper trustworthy in automation. Run this from the same directory where you saved conv.py; otherwise the import fails with ModuleNotFoundError.
How It Works
How It Works
Python is dynamically typed: a variable is just a name bound to an object, and the type lives on the object, not the name. That is why x = 42 followed by x = "hello" is legal; the name is rebound to a different object with a different type.
Type hints are annotations for readers and tools, not runtime contracts. Python never checks them when the code runs, which is why isinstance() guards and conversion helpers still matter at runtime: hints document intent, guards enforce it.
The to_int pattern used here is a tiny example of defensive programming for automation. Scripts consume config files, CLI arguments, and API responses, all of which arrive as strings of uncertain quality; a helper that converts-or-defaults keeps one bad value from crashing a whole run.
(string)"] --> TRY{"int(s)?"} TRY -->|"ok"| OK["int value"] TRY -->|"ValueError / TypeError"| DEF["default value"] OK --> USE["safe downstream use"] DEF --> USE
Validation & Testing
Feed valid, invalid, and None inputs to the converter and confirm it never raises. Pass criteria: valid inputs cast correctly, invalid ones fall back to the default, and type hints pass mypy.
Advanced: Troubleshooting
- Silent wrong type: Python won't warn; add
isinstancechecks at boundaries. - float precision: use
decimal.Decimalfor currency, not float. - Mutable defaults: never use a mutable default argument; use None and assign inside.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| isinstance check passes but code still fails | a subclass or duck-typed object satisfied the check but lacks the method used | check for the specific behaviour you need, or catch the exception at the use site |
| mypy reports errors the code never hits | hints describe intent; runtime never enforces them | treat mypy as a linter: fix the annotation or the code, then re-run |
| int('3.5') raises ValueError | int() does not parse float-formatted strings | convert via float() first, then int(), when fractional strings are valid input |
| mutable default argument keeps state | def f(items=[]) evaluates the list once | use None as the default and create the list inside the function |
| bool('False') is True | any non-empty string is truthy | parse boolean strings explicitly instead of relying on bool() |
Key Results
- Built conversion helpers that handle 100% of bad input without raising.
- Annotated functions with type hints validated by mypy.
- Documented the 5 core scalar types and their conversion pitfalls.
- Established defensive input patterns reused across later scripts.
Key Takeaways
- Types live on objects, not names: Python variables are just bindings.
- Type hints document intent for humans and tools; isinstance() guards enforce it at runtime.
- Centralize risky conversions in helpers that return a default on failure.
- Know mutability: aliasing surprises come from shared mutable objects.
- Test helpers with both good and bad input before trusting them in automation.
Practice Challenges
Challenge 1: Strict parser
Goal: Extend to_int into to_number that accepts int strings, float strings, and returns the default for anything else, with type hints.
Hint: Try float(s) as a fallback before giving up; keep the same default pattern.
Challenge 2: Truthy trap
Goal: Write to_bool that maps 'true'/'yes'/'1' to True and 'false'/'no'/'0' to False case-insensitively, raising on anything else.
Hint: Normalize with s.strip().lower() first, then compare against explicit sets.