Python Basics: Variables, Types, and Control Flow
Why Python variables work nothing like Java — and why that makes them more powerful
Abstract AlgorithmsAI-assisted content. This post may have been written or enhanced with AI tools. Please verify critical information independently.
TLDR: Python variables are labels that point at objects — not typed boxes. The type lives with the object, not the variable. Master truthiness, f-strings,
for/whileloops, and the handful of pitfalls that trip up every developer coming from Java or JavaScript, and you'll write idiomatic Python from day one.
📖 Why Python Variables Work Nothing Like Java: A Developer's First Day Rethinking the Basics
Maria has five years of Java experience. She sits down to write her first Python script and produces this:
# Maria's first Python — it runs, but it's not Pythonic
int x = 5 # SyntaxError — this is line 1, and it already fails
String name = "Alice"
boolean flag = True
The first line throws a SyntaxError. She removes the type declarations:
x = 5
name = "Alice"
flag = True
It runs. But then Maria tries to "be safe" and checks types manually everywhere, avoids reassigning variables to different types, and writes if name != null: — which fails, because Python doesn't have null. It has None.
Three hours later she has 80 lines of Python that reads like Java without semicolons. It works, but every Python developer who reviews it will wince.
The root issue is not syntax. It's a mental model mismatch. Java variables are typed containers — once you declare int x, that box can only hold integers. Python variables work completely differently, and once you internalize the actual model, everything else — truthiness, None, reassignment, dynamic behavior — snaps into place.
This post builds that mental model from scratch.
🔍 The Label Model: How Python Actually Stores Variables in Memory
In Java, a variable is a named container with a fixed type. When you write int x = 5, you reserve a box labelled x that holds only integers. The type is a property of the box.
In Python, a variable is a label (reference) that points at an object. The type is a property of the object, not the label. You can move the label to a different object at any time — even one of a different type.
Think of it like sticky notes on physical objects: the note says x, and it's stuck to the number 5. You can peel the note off and stick it on the string "hello" and Python won't complain.
x = 5 # The label 'x' points at the integer object 5
print(type(x)) # <class 'int'>
x = "hello" # The label 'x' now points at the string object "hello"
print(type(x)) # <class 'str'>
x = [1, 2, 3] # Now 'x' points at a list
print(type(x)) # <class 'list'>
You can observe this with two built-in tools: type() returns the type of the object a variable points at, and id() returns the memory address of that object.
a = 42
b = 42
print(id(a)) # e.g., 140234567890
print(id(b)) # same address — Python caches small integers!
print(a is b) # True — they point at the SAME object
a = 1000
b = 1000
print(a is b) # False — large integers are NOT cached; different objects
This is integer caching: CPython pre-allocates integer objects from -5 to 256. Any variable assigned a value in that range will point to the same shared object, making is comparisons return True. Outside that range, each assignment creates a new object.
The diagram below shows the Python object model for a simple variable reassignment. The variable x is a reference in the local namespace — it doesn't hold a value directly, it points to an object in the heap. Reassigning x moves the arrow; the original object 5 is not modified. When no variable points to an object, the garbage collector reclaims it.
flowchart TD
A[Variable x in namespace] -->|points to| B[Object: int 5 at 0xA1]
A2[Variable x after reassignment] -->|points to| C[Object: str hello at 0xB2]
B -.->|no references - GC eligible| D[Garbage Collected]
The key takeaway: moving the label never modifies the original object. This is why Python strings and integers are immutable — you can only point your label somewhere new; you cannot change the underlying object in place.
⚙️ Python's Built-in Types: int, float, str, bool, None and Their Surprising Behaviors
Python has a compact set of built-in scalar types. Here are the six you'll reach for constantly — including the surprising behaviors of each.
int — Arbitrary Precision by Default
Python integers never overflow. There's no int vs long distinction. Python will happily compute 2 ** 10000 without overflow.
big = 2 ** 100
print(big) # 1267650600228229401496703205376
print(type(big)) # <class 'int'>
float — IEEE 754, with All Its Surprises
print(0.1 + 0.2) # 0.30000000000000004 — classic float imprecision
print(0.1 + 0.2 == 0.3) # False!
# Use math.isclose() for float comparisons
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True
str — Immutable Unicode Sequences
greeting = "Hello, World!"
print(type(greeting)) # <class 'str'>
greeting[0] = "h" # TypeError: 'str' object does not support item assignment
bool — A Subclass of int
True and False are instances of bool, which is a subclass of int. This means True == 1 and False == 0 — they participate in arithmetic.
print(True + True) # 2
print(False * 100) # 0
print(isinstance(True, int)) # True
None — Python's Null, But Not Quite
None is a singleton — there is exactly one None object in a Python process. It represents the absence of a value. Always compare it with is, not ==:
result = None
print(result is None) # True (correct — identity check)
print(result == None) # True (works but can be overridden by __eq__)
Truthiness: What Python Considers False
Every Python object has a truth value, not just booleans. This is one of the most powerful (and most surprising) aspects of the language.
| Value | Truthy? | Why |
True | Yes | Boolean True |
1, 42, -1 | Yes | Non-zero number |
"hello" | Yes | Non-empty string |
[1, 2] | Yes | Non-empty list |
False | No | Boolean False |
0 | No | Zero integer |
0.0 | No | Zero float |
"" | No | Empty string |
[] | No | Empty list |
{} | No | Empty dict |
None | No | Null sentinel |
This lets you write expressive conditionals without explicit comparisons:
name = input("Enter your name: ")
if name: # True if non-empty string
print(f"Hello, {name}!")
else:
print("No name provided.")
items = []
if not items: # True if empty list
print("The list is empty.")
🌍 Strings in Real Python Code: f-strings, Slicing, and the Five Methods You'll Use Every Day
Strings in Python are rich, immutable sequences. Here's what you'll actually use in production code.
f-strings: The Modern Standard (Python 3.6+)
Forget % formatting and .format(). f-strings are the standard:
name = "Alice"
age = 30
score = 98.567
# Old ways (still valid, but verbose)
print("Hello, %s. You are %d years old." % (name, age))
print("Hello, {}. You are {} years old.".format(name, age))
# Modern: f-string
print(f"Hello, {name}. You are {age} years old.")
print(f"Score: {score:.2f}") # Format spec: 2 decimal places -> 98.57
print(f"Double age: {age * 2}") # Expressions are evaluated inline
String Slicing: [start:stop:step]
Strings support Python's full slice syntax. The rule: start is inclusive, stop is exclusive.
text = "Python Programming"
print(text[0:6]) # "Python" — chars at index 0,1,2,3,4,5
print(text[7:]) # "Programming" — from index 7 to end
print(text[:6]) # "Python" — from start to index 5
print(text[-11:]) # "Programming" — last 11 characters
print(text[::2]) # "Pto rgamn" — every other character
print(text[::-1]) # "gnimmargorP nohtyP" — reversed string
Five String Methods You'll Use Every Single Day
raw = " hello, world! "
# 1. strip() — remove leading/trailing whitespace
print(raw.strip()) # "hello, world!"
# 2. split() — split into a list by delimiter
csv_line = "Alice,30,Engineer"
parts = csv_line.split(",")
print(parts) # ['Alice', '30', 'Engineer']
# 3. join() — assemble a list back into a string
words = ["Python", "is", "readable"]
print(" ".join(words)) # "Python is readable"
# 4. replace() — substitute substrings
print("I love Java".replace("Java", "Python")) # "I love Python"
# 5. lower() / upper() / title() — case normalisation
email = " Alice@Example.COM "
print(email.strip().lower()) # "alice@example.com"
Raw Strings and Multi-line Strings
# Raw string: backslashes are literal (great for regex and file paths)
path = r"C:\Users\Alice\Documents"
print(path) # C:\Users\Alice\Documents
# Multi-line string with triple quotes
message = """
Dear Alice,
Your order has shipped.
"""
print(message.strip())
📊 Control Flow Visualized: if, elif, else, Ternary, and the match Statement
Python uses indentation to define blocks. No braces. No semicolons. The colon at the end of if, elif, else, for, while, and def is mandatory.
score = 85
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
else:
grade = "F"
print(f"Grade: {grade}") # Grade: B
Ternary Expression: Inline if/else
age = 20
status = "adult" if age >= 18 else "minor"
print(status) # "adult"
The match Statement (Python 3.10+)
Python's match statement is pattern matching — far more powerful than a Java switch. It matches against values, types, sequences, and even attribute patterns:
command = "quit"
match command:
case "quit":
print("Exiting...")
case "help":
print("Available commands: quit, help, status")
case "status":
print("All systems operational.")
case _:
print(f"Unknown command: {command}")
Short-Circuit Evaluation
Python and and or are short-circuit operators — they stop evaluating as soon as the result is determined. This is useful and intentional:
user = None
# Without short-circuit, this would raise AttributeError
name = user and user.get("name") # user is None (falsy) → stops; name = None
# or returns the first truthy value
display = name or "Anonymous"
print(display) # "Anonymous"
The diagram below shows how Python evaluates an if / elif / else chain. Each condition is tested in order; the first branch that evaluates to truthy executes and the rest are skipped entirely. If no condition is truthy, the optional else block runs as the fallback. This top-down short-circuit evaluation is why the order of your elif branches matters for both correctness and performance.
flowchart TD
A[Evaluate if condition] -->|True| B[Execute if block - done]
A -->|False| C[Evaluate elif condition]
C -->|True| D[Execute elif block - done]
C -->|False| E{More elif?}
E -->|Yes| C
E -->|No| F[Execute else block]
🧪 Loops and Functions: Progressive Examples from Range to First-Class Objects
for Loops Over Iterables — Not Indices
Python's for loop iterates over any iterable directly. You never need an index to traverse a list:
fruits = ["apple", "banana", "cherry"]
# Java-style (works, but un-Pythonic)
for i in range(len(fruits)):
print(fruits[i])
# Pythonic
for fruit in fruits:
print(fruit)
range() for Numeric Sequences
for i in range(5): # 0, 1, 2, 3, 4
print(i)
for i in range(1, 6): # 1, 2, 3, 4, 5
print(i)
for i in range(0, 10, 2): # 0, 2, 4, 6, 8 — step of 2
print(i)
enumerate() — Index and Value Together
Never write range(len(items)) to get an index. Use enumerate():
languages = ["Python", "Java", "Go", "Rust"]
# Un-Pythonic
for i in range(len(languages)):
print(f"{i}: {languages[i]}")
# Pythonic — enumerate gives you (index, value) tuples
for i, lang in enumerate(languages):
print(f"{i}: {lang}")
# Output:
# 0: Python
# 1: Java
# 2: Go
# 3: Rust
zip() — Iterate Two Lists in Lockstep
names = ["Alice", "Bob", "Carol"]
scores = [92, 87, 95]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# Alice: 92
# Bob: 87
# Carol: 95
while with break and continue
n = 10
total = 0
i = 0
while i < n:
if i % 2 == 0: # skip even numbers
i += 1
continue
total += i
i += 1
print(total) # 25 (sum of 1+3+5+7+9)
The Loop-Else Clause: Underused, Often Surprising
Python's for and while loops have an optional else clause that runs only if the loop completed normally — meaning it was NOT exited via break. This is perfect for search patterns:
def find_prime_factor(n):
for i in range(2, n):
if n % i == 0:
print(f"{n} is divisible by {i}")
break
else:
# Only runs if loop finished without break
print(f"{n} is prime!")
find_prime_factor(17) # 17 is prime!
find_prime_factor(18) # 18 is divisible by 2
The flow through loops and their branches is worth visualising. The diagram below shows how for/while loops evaluate their condition or iterable, execute the body, optionally break early, and — crucially — only reach the else block when no break was hit. This is the mechanism that makes the prime-factor search idiom clean and explicit.
flowchart TD
A[Start loop] --> B{More items or condition true?}
B -->|Yes| C[Execute loop body]
C --> D{break encountered?}
D -->|Yes| E[Exit loop - skip else]
D -->|No| B
B -->|No - exhausted| F[Run else block]
F --> G[Continue after loop]
E --> G
Functions as First-Class Citizens: Defining, Returning, and Passing Behavior
Functions in Python are objects — you can assign them to variables, pass them as arguments, and return them from other functions. For now, let's cover the fundamentals.
def greet(name, greeting="Hello"):
"""Return a formatted greeting string."""
return f"{greeting}, {name}!"
print(greet("Alice")) # "Hello, Alice!"
print(greet("Bob", "Hi")) # "Hi, Bob!"
print(greet(greeting="Hey", name="Carol")) # keyword arguments
A function with no return statement implicitly returns None:
def print_twice(msg):
print(msg)
print(msg)
result = print_twice("hello")
print(result) # None
args and *kwargs: Variable-Length Arguments (Preview)
def total(*args):
"""Sum any number of arguments."""
return sum(args)
print(total(1, 2, 3)) # 6
print(total(10, 20, 30, 40)) # 100
def describe(**kwargs):
"""Print key=value pairs."""
for key, value in kwargs.items():
print(f" {key}: {value}")
describe(name="Alice", age=30, role="Engineer")
# name: Alice
# age: 30
# role: Engineer
Functions are first-class: you can store them in variables and pass them around:
def square(x):
return x * x
def apply(func, value):
return func(value)
print(apply(square, 5)) # 25
⚠️ Pitfalls That Trip Up Java and JavaScript Developers Learning Python
These are not edge cases. Every developer coming from a typed language will hit at least three of these in their first week.
1. Mutable Default Arguments Are a Trap
This is one of the most common Python bugs for newcomers:
# BUG: the default list [] is created ONCE when the function is defined
def append_to(element, to=[]):
to.append(element)
return to
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] ← NOT [2] — the list persists across calls!
print(append_to(3)) # [1, 2, 3]
# FIX: use None as sentinel and create a fresh list inside
def append_to_safe(element, to=None):
if to is None:
to = []
to.append(element)
return to
print(append_to_safe(1)) # [1]
print(append_to_safe(2)) # [2] — fresh list each time
2. == vs is: Equality vs Identity
== checks if two objects have the same value. is checks if they are the same object in memory. For None, always use is:
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True — same values
print(a is b) # False — different objects
# Only correct way to check for None
result = None
print(result is None) # True (correct)
print(result == None) # True (works but fragile — a custom __eq__ can break it)
3. Integer Division Always Returns float in Python 3
# Python 3 — / always returns float
print(7 / 2) # 3.5 (not 3!)
# Use // for floor division (integer result)
print(7 // 2) # 3
# This is a common source of off-by-one bugs when porting from Python 2 or Java
4. Integers Don't Overflow in Python
# Java: Integer.MAX_VALUE + 1 wraps to a negative number
# Python: integers grow without bound
x = 2 ** 1000
print(x) # A 302-digit number — no overflow, no exception
5. The LEGB Scope Rule (Preview)
Python resolves names in four scopes in this order: Local → Enclosing → Global → Built-in. If you assign a name inside a function, Python treats it as a local variable for that entire function — even lines before the assignment:
x = 10
def bad():
print(x) # UnboundLocalError: referenced before assignment
x = 20 # This assignment makes x local to the entire function
# Fix: use 'global' keyword (sparingly) or restructure to avoid global state
def good():
global x
print(x) # 10
x = 20
🛠️ Python Type Tooling: How mypy and Type Hints Bring Discipline to Dynamic Code
Python is dynamically typed, but that doesn't mean you have to fly blind. Since Python 3.5, you can add type hints — optional annotations that document expected types. They don't change runtime behavior, but they enable static analysis tools to catch bugs before you run the code.
mypy is the leading open-source static type checker for Python. You install it once and run it against your codebase.
# Without type hints — valid Python, but mypy can't help you here
def add(a, b):
return a + b
# With type hints — mypy can now verify callers
def add_typed(a: int, b: int) -> int:
return a + b
result: int = add_typed(3, 4) # OK
wrong = add_typed("hello", 4) # mypy error: Argument 1 to "add_typed" has incompatible type "str"; expected "int"
You can annotate variables, function parameters, return types, and collection contents:
from typing import Optional, list
def find_user(user_id: int) -> Optional[str]:
"""Returns the username if found, None otherwise."""
users = {1: "Alice", 2: "Bob"}
return users.get(user_id)
# Runtime type checking with isinstance()
def process(value: int | str) -> str:
if isinstance(value, int):
return f"Number: {value}"
return f"Text: {value}"
print(process(42)) # "Number: 42"
print(process("hello")) # "Text: hello"
To run mypy, install it and point it at your file:
pip install mypy
mypy my_script.py
mypy will report type mismatches, missing return type annotations, and incorrect argument types — all without executing the code. It integrates with VS Code, PyCharm, and CI pipelines. For a growing Python codebase, adding type hints incrementally (start with function signatures) gives you the safety of static typing without abandoning Python's flexibility.
For a full deep-dive on type hints and mypy configurations, see the Python Type Safety companion post (planned).
📚 Lessons Learned from Teaching Python to Java Developers
After watching many experienced developers learn Python, the same friction points appear every time:
The label mental model takes 48 hours to actually land. Developers understand it intellectually in ten minutes, but then continue writing Java-style null checks and type assertions for days. The model only sticks after seeing a mutable default argument bug in their own code.
Truthiness is either loved or hated — never neutral. Java developers initially resist if items: because it feels ambiguous. After two weeks, they won't go back. But skipping the truthiness table and jumping straight to examples causes bugs — "what does an empty dict evaluate to again?"
is vs == creates a specific class of bugs. Developers from null-safe languages naturally write if result == None and it works fine until a library object overrides __eq__. Teaching is None as a rule, not a preference, prevents the bug class entirely.
The loop-else clause surprises everyone. Even experienced Pythonistas encounter it as if for the first time. The example of using it for a "found" / "not found" search loop makes it immediately useful rather than a curiosity.
f-strings should be taught on day one. Developers who learn % formatting or .format() first will use those for years. Showing f-strings before either alternative prevents technical debt in new codebases.
Type hints pay dividends in team codebases. Developers initially resist annotations as "extra typing." Once they see mypy catch a None dereference in a code review before it hits staging, they add annotations proactively.
📌 TLDR: Python Basics in 10 Key Takeaways
- Variables are labels, not typed boxes. The type lives with the object, not the variable. You can reassign a variable to any type at any time.
type()reveals the object's type;id()reveals its memory address. CPython caches integers from -5 to 256 —iscomparisons on small ints areTruebut don't rely on it.- Every object has a truth value. Falsy:
0,"",[],{},None,False. All others are truthy. Use this to write cleaner conditionals. - f-strings are the modern string formatting standard. Use
f"{variable:.2f}"for inline expressions and format specs. - String slicing is
[start:stop:step]— start inclusive, stop exclusive.[::-1]reverses a string. foriterates over iterables directly. Useenumerate()for index + value,zip()for two lists in lockstep. Never writerange(len(items)).- Loop-else runs only when no
breakwas hit. It's the cleanest pattern for "search and report not found" logic. - Never use mutable objects as default arguments. Use
Noneas the sentinel and create a fresh collection inside the function. ischecks identity (same object);==checks equality (same value). Always useisforNonecomparisons.- Type hints + mypy give you static analysis without losing dynamic flexibility. Add annotations to function signatures first, then propagate inward.
📝 Practice Quiz: Test What You Actually Learned
- What does the following code print?
a = 256
b = 256
print(a is b)
c = 1000
d = 1000
print(c is d)
Answer
Correct Answer:True then False.
CPython caches integers from -5 to 256 — so a and b point to the same object (identity check passes). The integer 1000 is outside the cache range, so CPython creates two separate objects, making c is d return False. This is why you should never use is to compare arbitrary integers — use == for value equality.
- A function is called three times. What does it print each time, and why?
def add_item(item, cart=[]):
cart.append(item)
return cart
print(add_item("apple"))
print(add_item("banana"))
print(add_item("cherry"))
Answer
Correct Answer:['apple']
['apple', 'banana']
['apple', 'banana', 'cherry']
The default list [] is created once when the function is defined, not on each call. Every call shares the same list object. Fix: use cart=None as the default and create cart = [] inside the function body when cart is None.
- Which of the following values are falsy in Python?
0 · "" · "False" · None · [] · [0] · 0.0 · {}
Answer
Correct Answer:0, "", None, [], 0.0, {} are falsy.
Truthy: "False" (non-empty string), [0] (non-empty list — even though it contains zero).
The string "False" is truthy because it is a non-empty string. [0] is truthy because the list itself is non-empty, regardless of what it contains.
- What is the output of this code?
result = 7 / 2
print(result)
print(type(result))
result2 = 7 // 2
print(result2)
print(type(result2))
Answer
Correct Answer:3.5
<class 'float'>
3
<class 'int'>
In Python 3, / always performs true division and returns a float. // performs floor (integer) division and returns an int. This is a breaking change from Python 2, where / between two integers returned an integer.
- What does the loop-else clause print, and under what condition does the
elseblock execute?
target = 7
numbers = [2, 4, 6, 8, 10]
for n in numbers:
if n == target:
print(f"Found {target}")
break
else:
print(f"{target} not found in list")
Answer
Correct Answer: It prints7 not found in list.
The else block of a for loop runs only when the loop completes without hitting a break. Because 7 is not in the list, the loop exhausts all items without breaking, and the else block executes. If target were 6, the loop would break and the else block would be skipped entirely.
- Rewrite this un-Pythonic loop using
enumerate():
items = ["pen", "notebook", "ruler"]
for i in range(len(items)):
print(f"{i}: {items[i]}")
Answer
Correct Answer:python
items = ["pen", "notebook", "ruler"]
for i, item in enumerate(items):
print(f"{i}: {item}")
enumerate(items) yields (index, value) tuples, eliminating the need for range(len(...)) and the manual index lookup. You can also start the counter at 1: enumerate(items, start=1).
🔗 Related Posts
- The Ultimate Data Structures Cheat Sheet — once you're comfortable with Python syntax, this cheat sheet bridges you directly into algorithmic thinking with lists, trees, heaps, and graphs
- Apache Spark for Data Engineers: RDDs, DataFrames, and Structured Streaming — Python is the primary language for Spark's PySpark API; this post is the natural next step for data engineering work
- Python Programming Roadmap — the full learning path for this series, from basics through functions, OOP, and testing

Written by
Abstract Algorithms
@abstractalgorithms
More Posts
RAG vs Fine-Tuning: When to Use Each (and When to Combine Them)
TLDR: RAG gives LLMs access to current knowledge at inference time; fine-tuning changes how they reason and write. Use RAG when your data changes. Use fine-tuning when you need consistent style, tone, or domain reasoning. Use both for production assi...
Fine-Tuning LLMs with LoRA and QLoRA: A Practical Deep-Dive
TLDR: LoRA freezes the base model and trains two tiny matrices per layer — 0.1 % of parameters, 70 % less GPU memory, near-identical quality. QLoRA adds 4-bit NF4 quantization of the frozen base, enabling 70B fine-tuning on 2× A100 80 GB instead of 8...
Build vs Buy: Deploying Your Own LLM vs Using ChatGPT, Gemini, and Claude APIs
TLDR: Use the API until you hit $10K/month or a hard data privacy requirement. Then add a semantic cache. Then evaluate hybrid routing. Self-hosting full model serving is only cost-effective at > 50M tokens/day with a dedicated MLOps team. The build ...
Watermarking and Late Data Handling in Spark Structured Streaming
TLDR: A watermark tells Spark Structured Streaming: "I will accept events up to N minutes late, and then I am done waiting." Spark tracks the maximum event time seen per partition, takes the global minimum across all partitions, subtracts the thresho...
