Skip to content

Python Basics — Variables, Data Types, and Control Flow

Python reads like English, runs everywhere, and has the best data ecosystem of any language. This file covers the fundamentals you need before touching any data — the building blocks every Python program is made from. Even if you have written some Python before, work through the examples: the nuances around type conversion and None are the source of real bugs in data pipelines.

Learning Objectives

  • Create and use variables with meaningful names
  • Work with Python's core data types: int, float, str, bool, None
  • Format output cleanly with f-strings
  • Write conditional logic with if, elif, else
  • Loop with for and while, using break and continue
  • Write list comprehensions for compact transformations
  • Understand Python indentation as syntax, not style

Variables and Assignment

Python is dynamically typed — you don't declare a type, you just assign a value.

# Variable assignment
revenue = 125000
growth_rate = 0.15
company_name = "DataCo Ltd"
is_profitable = True
delivery_date = None          # Python's version of NULL

print(revenue)                # 125000
print(type(revenue))          # <class 'int'>
print(type(growth_rate))      # <class 'float'>
print(type(company_name))     # <class 'str'>
print(type(is_profitable))    # <class 'bool'>
print(type(delivery_date))    # <class 'NoneType'>

Naming conventions: - Use snake_case: total_revenue, customer_id, is_active - No spaces, no hyphens, do not start with a number - Be descriptive: rev saves 4 keystrokes but costs 10 seconds every time you read the code

# Multiple assignment in one line
total, average, count = 45000, 9000.0, 5

# Swap two values (Python-only feature — no temp variable needed)
a, b = 10, 20
a, b = b, a
print(a, b)    # 20 10

Core Data Types

Integers and Floats

# Integer
orders = 1450
avg_per_day = orders // 30      # integer division → 48 (no decimal)
remainder    = orders % 30      # modulo → 10
print(f"{orders} orders, approx {avg_per_day} per day, {remainder} extra")

# Float
revenue = 48_250.75             # underscores improve readability for large numbers
tax_rate = 0.18

# Arithmetic
gross = revenue * (1 + tax_rate)
net_margin = (revenue - 35_000) / revenue

print(f"Gross:  £{gross:,.2f}")         # £56,935.89   — comma separator, 2 dp
print(f"Margin: {net_margin:.1%}")      # 27.5%         — percentage format
print(f"Revenue: £{revenue:>12,.2f}")   # right-aligned in 12-char field

# Common operations
print(2 ** 10)           # 1024   — exponentiation
print(abs(-48.5))        # 48.5   — absolute value
print(round(3.14159, 2)) # 3.14   — round to 2 decimal places

Floating-point precision

0.1 + 0.2 in Python equals 0.30000000000000004, not 0.3. This is a property of IEEE 754 floating-point arithmetic, not a Python bug. For financial calculations, use the decimal module:

from decimal import Decimal
result = Decimal("0.1") + Decimal("0.2")   # Decimal('0.3') — exact
In practice, always round financial results before displaying or comparing them.

Strings

Strings are one of the most-used data types in analytics — almost every dataset has string columns for names, categories, and IDs.

name  = "Nikhil Sharma"
city  = "Mumbai"
score = 94.5

# f-strings (Python 3.6+) — the preferred way to build strings
greeting = f"Welcome back, {name}! You're in {city}."
result   = f"Score: {score:.1f}%"
print(greeting)   # Welcome back, Nikhil Sharma! You're in Mumbai.

# String methods — analysts use these every day
print(name.upper())                    # NIKHIL SHARMA
print(name.lower())                    # nikhil sharma
print(name.title())                    # Nikhil Sharma
print(name.split(" "))                 # ['Nikhil', 'Sharma']
print(name.replace("Nikhil", "Nick")) # Nick Sharma
print(name.startswith("N"))           # True
print(name.endswith("a"))             # True
print(len(name))                      # 13
print("  padded  ".strip())           # 'padded'   — removes leading/trailing whitespace
print("  padded  ".lstrip())          # 'padded  ' — left strip only
print("a,b,c".split(","))             # ['a', 'b', 'c']
print("-".join(["2024", "01", "15"])) # '2024-01-15'

# Checking content
print("INV" in "INV-2024-001")        # True — substring check
print("hello".isdigit())              # False
print("42".isdigit())                 # True
print("hello world".count("l"))       # 3
print("hello world".find("world"))    # 6  — index of first occurrence (-1 if not found)

# Multi-line string (triple quotes)
report = """
Monthly Revenue Report
----------------------
Revenue: £48,250.75
Growth:  +15.2%
"""
print(report.strip())   # strip removes leading/trailing blank lines

For mid-level analysts

String methods like .strip(), .lower(), and .replace() are your first line of defence when cleaning messy data. Run them on every string column you load from a CSV before doing any comparisons. A column with " Completed " (spaces) and "completed" (lowercase) will not match "completed" without cleaning first.

Boolean and None

is_active   = True
has_discount = False

# Boolean operations
print(is_active and has_discount)   # False — both must be True
print(is_active or has_discount)    # True  — at least one is True
print(not is_active)                # False

# None — Python's equivalent of NULL in SQL
delivery_date = None
print(delivery_date is None)        # True  — correct way to check for None
print(delivery_date is not None)    # False
print(delivery_date == None)        # True  — works, but is not None is preferred

# Why is None?
# None represents the absence of a value.
# In data work, it appears when a field was not filled in.
# When you load a CSV, missing cells become None (or np.nan in Pandas).

Use is None, not == None

== None works but is frowned upon. is None checks object identity — there is only one None object in Python's memory, so identity is the right check. Linters will flag == None in production code.

Type Conversion

Data from CSV files always arrives as strings. Type conversion is a daily task.

# String → number
revenue_str = "48,250.75"
revenue = float(revenue_str.replace(",", ""))    # remove comma first → 48250.75
orders  = int("1450")                             # 1450

# Number → string
label = "Revenue: £" + str(revenue)              # must convert before concatenating

# bool() conversion — understanding truthiness
print(bool(0))        # False — zero is falsy
print(bool(1))        # True  — any non-zero number is truthy
print(bool(""))       # False — empty string is falsy
print(bool("hello"))  # True  — non-empty string is truthy
print(bool(None))     # False — None is falsy
print(bool([]))       # False — empty list is falsy
print(bool([0]))      # True  — non-empty list is truthy, even if it contains 0

# Safe conversion with error handling
def safe_float(value, default=0.0):
    """Convert value to float, return default if conversion fails."""
    try:
        return float(str(value).replace(",", "").replace("£", "").replace("$", "").strip())
    except (ValueError, TypeError):
        return default

print(safe_float("£1,249.50"))   # 1249.5
print(safe_float("N/A"))          # 0.0
print(safe_float(None))           # 0.0
print(safe_float(389.00))         # 389.0

Implicit type conversion in Python is limited

Unlike JavaScript, "10" + 10 raises a TypeError in Python. You must convert explicitly. This saves you from subtle bugs when reading data from CSV files where numbers arrive as strings.


Comparison Operators

revenue = 48_250.75
target  = 50_000

print(revenue > target)          # False
print(revenue >= 40_000)         # True
print(revenue == 48_250.75)      # True
print(revenue != target)         # True

# Chained comparisons — a Python exclusive
print(40_000 <= revenue <= 50_000)   # True — very readable, works correctly

# String comparison (alphabetical)
print("banana" < "cherry")   # True
print("Z" < "a")             # True — uppercase letters come before lowercase in ASCII

# Identity vs equality
a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)    # True  — same values
print(a is b)    # False — different objects in memory
print(a is c)    # True  — same object (c is just another name for a)

Conditional Logic

revenue = 48_250.75
target  = 50_000

if revenue >= target:
    print("Target achieved!")
    bonus = revenue * 0.05
elif revenue >= target * 0.9:
    print("Within 10% of target — close!")
    bonus = revenue * 0.02
elif revenue >= target * 0.75:
    print("Missed target significantly")
    bonus = 0
else:
    print(f"Shortfall: £{target - revenue:,.2f}")
    bonus = 0

# Inline conditional (ternary expression)
status = "On Track" if revenue >= target * 0.9 else "Off Track"
print(status)    # On Track

# Using conditions with None safely
delivery_date = None
days_remaining = (delivery_date - pd.Timestamp.today()).days if delivery_date is not None else "Not scheduled"

Common Pattern — Classify and Label

def classify_revenue(revenue):
    """Classify revenue into performance tiers."""
    if revenue >= 60_000:
        return "Excellent"
    elif revenue >= 50_000:
        return "Good"
    elif revenue >= 40_000:
        return "Acceptable"
    else:
        return "Below Target"

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
revenues = [42_000, 38_500, 51_200, 49_800, 55_100, 48_250]

for month, rev in zip(months, revenues):
    tier = classify_revenue(rev)
    print(f"{month}: £{rev:,}{tier}")

Output:

Jan: £42,000 — Acceptable
Feb: £38,500 — Below Target
Mar: £51,200 — Good
Apr: £49,800 — Acceptable
May: £55,100 — Good
Jun: £48,250 — Acceptable


Loops

for loop — iterate over a sequence

months   = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
revenues = [42_000, 38_500, 51_200, 49_800, 55_100, 48_250]

# Basic iteration
for rev in revenues:
    print(rev)

# zip — iterate over two lists in parallel
for month, rev in zip(months, revenues):
    flag = "✓" if rev >= 50_000 else "✗"
    print(f"{month}: £{rev:,} {flag}")

# enumerate — get index and value together
for i, month in enumerate(months, start=1):
    print(f"  {i}. {month}")

# range — generate number sequences
for i in range(5):            # 0, 1, 2, 3, 4
    print(i)

for i in range(1, 11):        # 1 to 10 inclusive
    print(i)

for i in range(0, 100, 10):  # 0, 10, 20, ... 90
    print(i)

Calculating running values in a loop

revenues = [42_000, 38_500, 51_200, 49_800, 55_100, 48_250]

# Running total
running_total = 0
for i, rev in enumerate(revenues):
    running_total += rev
    print(f"Month {i+1}: revenue £{rev:,}, cumulative £{running_total:,}")

# Month-over-month growth
for i in range(1, len(revenues)):
    prev = revenues[i - 1]
    curr = revenues[i]
    growth = (curr - prev) / prev * 100
    direction = "↑" if growth > 0 else "↓"
    print(f"Month {i+1}: {direction} {abs(growth):.1f}%")

break and continue

# break — stop the loop immediately
revenues = [42_000, 38_500, 51_200, 49_800, 55_100, 48_250]
target = 55_000

for i, rev in enumerate(revenues):
    if rev >= target:
        print(f"First month to hit £{target:,}: Month {i+1}{rev:,})")
        break    # stop — we found what we needed
else:
    # The for...else block runs if the loop completed WITHOUT break
    print("No month reached the target")

# continue — skip this iteration, move to next
for rev in revenues:
    if rev < 40_000:
        print(f"Skipping weak month: £{rev:,}")
        continue    # go to next iteration
    print(f"Processing: £{rev:,}")

while loop — repeat until a condition is false

Use while when you don't know in advance how many iterations you need.

# Budget allocation
budget = 10_000
items  = [2_500, 3_200, 1_800, 4_500, 900]
spent  = 0
idx    = 0

while idx < len(items) and spent + items[idx] <= budget:
    spent += items[idx]
    print(f"Approved: £{items[idx]:,} | Remaining: £{budget - spent:,}")
    idx += 1

print(f"\nTotal approved: £{spent:,}")
if idx < len(items):
    print(f"Stopped at item {idx+1} — £{items[idx]:,} would exceed budget")

Infinite loops

A while loop runs forever if its condition never becomes False. Always make sure the condition will eventually be False, or include a break statement. An infinite loop will crash your notebook kernel.


List Comprehensions

A compact way to build lists. One of Python's most practical features.

revenues = [42_000, 38_500, 51_200, 49_800, 55_100, 48_250]

# Standard loop approach
above_target = []
for rev in revenues:
    if rev >= 50_000:
        above_target.append(rev)

# List comprehension — equivalent, shorter, faster
above_target = [rev for rev in revenues if rev >= 50_000]
print(above_target)   # [51200, 55100]

# Apply a transformation
in_thousands = [rev / 1_000 for rev in revenues]
print(in_thousands)   # [42.0, 38.5, 51.2, 49.8, 55.1, 48.25]

# Transform and filter together
large_formatted = [f{rev:,}" for rev in revenues if rev > 48_000]
print(large_formatted)   # ['£51,200', '£49,800', '£55,100', '£48,250']

# Nested list comprehension — flat list from nested list
quarterly_data = [[42_000, 38_500, 51_200], [49_800, 55_100, 48_250]]
flat = [rev for quarter in quarterly_data for rev in quarter]
print(flat)   # [42000, 38500, 51200, 49800, 55100, 48250]

For mid-level analysts

Use list comprehensions when the transformation is simple and the result is a list. When you have complex multi-step logic, keep the for loop — readability matters more than brevity. The rule: if a comprehension requires more than one line to understand, write a loop.


Python Indentation — It's Syntax

Python uses indentation (4 spaces per level) to define code blocks. This is not a style preference — inconsistent indentation causes errors.

# CORRECT — 4 spaces per level
for item in [100, 50, 200, 30]:
    if item > 100:
        print(f"Large item: {item}")
        flag = "L"
    else:
        print(f"Small item: {item}")
        flag = "S"

# WRONG — causes IndentationError
for item in [100, 50]:
print(item)          # Not indented — Python sees this as outside the loop

# WRONG — causes IndentationError
if True:
        pass   # 8 spaces when 4 are expected

Mixing tabs and spaces causes IndentationError

Use 4 spaces consistently. Pressing Tab in Jupyter Lab and VS Code inserts 4 spaces. If you copy code from a source that uses tabs, you may get a TabError when mixing. In VS Code, you can see whitespace characters with View → Render Whitespace.


F-String Formatting Reference

F-strings (f"...") are the modern Python way to embed values in strings. Available since Python 3.6.

name    = "Nikhil"
revenue = 48_250.756
count   = 1450
rate    = 0.1523

# Basic embedding
print(f"Hello, {name}!")                   # Hello, Nikhil!

# Numeric formatting
print(f"Revenue: £{revenue:.2f}")          # Revenue: £48250.76      — 2 decimal places
print(f"Revenue: £{revenue:,.2f}")         # Revenue: £48,250.76     — comma separator
print(f"Growth: {rate:.1%}")               # Growth: 15.2%           — percentage
print(f"Orders: {count:,}")                # Orders: 1,450           — comma in integer
print(f"Revenue: £{revenue:>12,.2f}")      # right-aligned in 12-char field
print(f"Name: {name:<15}|")               # left-aligned in 15-char field: 'Nikhil         |'

# Expressions inside f-strings
print(f"Doubled: £{revenue * 2:,.2f}")     # expressions work
print(f"Type: {type(revenue).__name__}")   # float

# Padding for aligned tables
for month, rev in [("Jan", 42000), ("Feb", 38500), ("Mar", 51200)]:
    print(f"{month:<5} £{rev:>10,}")
# Jan   £    42,000
# Feb   £    38,500
# Mar   £    51,200

Practice Exercises

Warm-up 1. Create variables for your name, age, and annual salary. Print an f-string: "My name is [name]. I am [age] years old and earn £[salary:,] per year." 2. Given revenues = [42000, 38500, 51200, 49800, 55100, 48250], use a for loop to print each revenue with a "✓" if above 50,000 and "✗" if not. 3. Write a conditional that classifies a customer's order total as "Small" (< £50), "Medium" (£50–£200), or "Large" (> £200).

Main 4. Use a list comprehension to extract only months where revenue exceeded £50,000 from the list above. 5. Write a loop that calculates month-over-month growth rates for the revenues list (skip the first month). Print each as "Month 2: +7.3%". 6. Write code that finds the maximum and minimum revenue in the list using only a loop and comparisons — do not use max() or min().

Stretch 7. Write a loop that iterates over a list of order totals and builds a running total, stopping (with break) and printing a message if the running total exceeds £100,000. Print a final summary of how many orders were processed. 8. Use a nested list comprehension to create a 5×5 times table as a list of lists: [[1*1, 1*2, ...], [2*1, 2*2, ...], ...]. Then print it as a formatted grid.


Interview Questions

[Beginner] What are Python's basic data types for analytics work?

Show answer

The core types are int (whole numbers), float (decimals), str (text), bool (True/False), and None (absence of value). For data structures: list (ordered, mutable), dict (key-value, mutable), tuple (ordered, immutable), set (unique values, unordered).

Mutability matters: lists and dicts can be changed after creation; strings, ints, tuples cannot. Passing a mutable object to a function and modifying it inside the function changes the original — a common source of bugs.


[Beginner] What is the difference between = and == in Python?

Show answer
  • = is assignment: x = 5 stores the value 5 in the variable x.
  • == is comparison: x == 5 evaluates to True or False.
x = 10          # store 10 in x
print(x == 10)  # True — is x equal to 10?
print(x == 5)   # False

A common mistake for beginners: if x = 5: is a SyntaxError in Python (though it's valid in some other languages). Python requires if x == 5:.


[Beginner] What does None represent in Python? How is it similar to NULL in SQL?

Show answer

None is Python's way of saying "no value". It is the equivalent of NULL in SQL and NA/NaN in spreadsheets.

delivery_date = None    # order placed but not yet dispatched

# Correct check
if delivery_date is None:
    print("Not yet dispatched")

# In Pandas, None in a numeric column becomes np.nan
# In Pandas, use .isna() or .notna() to check

Key difference from SQL: in SQL, NULL == NULL is False. In Python, None is None is True — there is exactly one None object. Use is None, not == None.


[Mid-level] What is a list comprehension? Rewrite this as a list comprehension:

result = []
for x in data:
    if x > 0:
        result.append(x * 2)

Show answer
result = [x * 2 for x in data if x > 0]

List comprehensions are faster than equivalent loops (they run in C internally), shorter, and more readable for simple transformations. The structure is: [expression for variable in iterable if condition]

Use a regular loop when: - The logic is complex (multiple conditions, nested logic) - You need to append to multiple lists - You need break or continue


[Mid-level] What is the difference between is and == in Python?

Show answer
  • == compares values: are the contents equal?
  • is compares identity: are they the same object in memory?
a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)   # True  — same values
print(a is b)   # False — different list objects
print(a is c)   # True  — c is just another name for a (same object)

For analytics: - Always use == to compare values - Use is only for None checks: if value is None - Never use is to compare strings or numbers — it may work by accident (Python caches small integers), but it's not reliable


[Senior] Explain how Python's for loop is actually using an iterator protocol. When would you use a generator instead of a list comprehension?

Show answer

Every for loop in Python calls iter() on the object, then repeatedly calls next() until StopIteration is raised. This is the iterator protocol. Lists, tuples, strings, dicts, and files all implement it.

# What Python does internally for: for x in [1, 2, 3]:
it = iter([1, 2, 3])
while True:
    try:
        x = next(it)
        # ... loop body
    except StopIteration:
        break

A generator is a function that yields values one at a time instead of building the full list:

# List comprehension — builds entire list in memory
squares = [x**2 for x in range(1_000_000)]  # ~8MB in memory

# Generator expression — produces one value at a time
squares = (x**2 for x in range(1_000_000))  # ~200 bytes in memory

# Generator function — same thing, with more control
def revenue_rows(filepath):
    import csv
    with open(filepath) as f:
        for row in csv.DictReader(f):
            yield row    # one row at a time — never loads file into memory

Use generators when: - Processing files larger than available RAM - Building data pipelines where each step feeds the next - You only need to iterate once (generators can't be rewound) - Computing running aggregations (sum, max) without storing all values


Previous: 00-agenda | Next: 02-functions