Introduction to Python · Lesson 1 of 5

Python Basics: Syntax, Variables & Types

Python Basics: Syntax, Variables, Data Types, and String Operations

Python is one of the most readable programming languages ever designed. Its creator, Guido van Rossum, intentionally made the syntax close to plain English. That readability comes with strict rules — especially around indentation — that beginners must understand from day one.

This guide covers everything from how Python thinks about whitespace, to variables, to every core data type, to the full string manipulation toolkit. By the end, you will write Python that is not just correct, but idiomatic.


1. Indentation: Python's Most Important Rule

Most languages use curly braces {} to group code blocks. Python uses indentation — the whitespace at the start of a line. This is not stylistic preference; it is syntax.

Python
# This is correct Python
if True:
    print("This line is inside the if block")
    print("So is this line")
print("This line is outside the if block")
Python
# This raises an IndentationError
if True:
print("Missing indentation")  # IndentationError!

The Golden Rule: Consistency

Python does not care whether you use 2 spaces, 4 spaces, or a tab — but you must be consistent within a block. The Python community has standardized on 4 spaces per level (PEP 8).

Python
# WRONG: mixing tabs and spaces (Python 3 raises TabError)
if True:
    print("4 spaces")
	print("tab")  # TabError!

# RIGHT: 4 spaces consistently
if True:
    print("level 1")
    if True:
        print("level 2")
        if True:
            print("level 3")

Why Indentation as Syntax?

This design forces readable code. In languages with braces, developers debate brace placement styles. In Python, there is only one way. The visual structure of your code is the structure of your program.

Python
# You can see the nesting at a glance
def process_users(users):
    for user in users:
        if user["active"]:
            if user["age"] >= 18:
                print(f"Adult active user: {user['name']}")
            else:
                print(f"Minor active user: {user['name']}")
        else:
            print(f"Inactive user: {user['name']}")

2. print() and input()

print()

print() outputs text to the console. It is the first function almost every Python developer learns.

Python
# Basic usage
print("Hello, World!")

# Multiple arguments  separated by space by default
print("Name:", "Alice", "Age:", 30)
# Output: Name: Alice Age: 30

# Custom separator
print("2026", "04", "17", sep="-")
# Output: 2026-04-17

# Custom end character (default is newline \n)
print("Loading", end="")
print("...", end="")
print(" Done!")
# Output: Loading... Done!

# Print nothing (empty line)
print()

# Print a list or other object
numbers = [1, 2, 3]
print(numbers)
# Output: [1, 2, 3]

Printing with Formatting

Python
name = "Alice"
score = 98.5
rank = 1

# Old-style % formatting (still seen in legacy code)
print("Student: %s, Score: %.1f" % (name, score))

# str.format() method
print("Student: {}, Score: {:.1f}".format(name, score))

# f-strings (modern, preferred since Python 3.6)
print(f"Student: {name}, Score: {score:.1f}, Rank: #{rank}")
# Output: Student: Alice, Score: 98.5, Rank: #1

input()

input() reads a line of text from the user. It always returns a string.

Python
# Basic input
name = input("What is your name? ")
print(f"Hello, {name}!")

# Input always returns string  convert when needed
age_str = input("How old are you? ")
age = int(age_str)  # Convert to integer
print(f"In 10 years you will be {age + 10}")

# Common mistake: forgetting to convert
age = input("Age: ")
# print(age + 10)  # TypeError: can only concatenate str (not "int") to str

# Safe input with validation
while True:
    try:
        age = int(input("Enter your age: "))
        break
    except ValueError:
        print("Please enter a valid number.")

3. Variables

Variables are labels that point to values stored in memory. Python is dynamically typed — you do not declare types, the interpreter infers them.

Variable Assignment

Python
# Simple assignment
x = 10
name = "Alice"
is_active = True

# Multiple assignment on one line
a, b, c = 1, 2, 3
print(a, b, c)  # 1 2 3

# Swap variables (Python's elegant idiom)
a, b = b, a
print(a, b)  # 2 1

# Assign the same value to multiple variables
x = y = z = 0
print(x, y, z)  # 0 0 0

# Augmented assignment
count = 0
count += 1   # count = count + 1
count += 1
print(count)  # 2

score = 100
score -= 15  # subtract
score *= 2   # multiply
score //= 3  # integer divide
print(score)  # 56

Naming Rules

Python
# Valid variable names
user_name = "Alice"        # snake_case (PEP 8 recommended)
userName = "Bob"           # camelCase (not recommended in Python)
_private = "secret"        # leading underscore = convention for private
__dunder__ = "special"     # double underscore = dunder/magic (avoid in user code)
MAX_SIZE = 100             # ALL_CAPS = convention for constants
counter1 = 0               # letters and digits (not at start)

# Invalid names
# 1counter = 0   # SyntaxError: cannot start with digit
# my-var = 0     # SyntaxError: hyphens not allowed
# class = "A"    # SyntaxError: reserved keyword

# Check reserved keywords
import keyword
print(keyword.kwlist)
# ['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await',
#  'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except',
#  'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is',
#  'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try',
#  'while', 'with', 'yield']

Variables Are References

Understanding that variables are references (not boxes) is crucial:

Python
# Integers are immutable  reassignment creates a new object
x = 5
y = x       # y points to the same 5
x = 10      # x now points to 10; y still points to 5
print(x, y)  # 10 5

# Lists are mutable  both variables point to the same list
list_a = [1, 2, 3]
list_b = list_a       # list_b is NOT a copy; it's the same list
list_a.append(4)
print(list_b)         # [1, 2, 3, 4] — list_b sees the change!

# To copy: use .copy() or list()
list_c = list_a.copy()
list_a.append(5)
print(list_c)         # [1, 2, 3, 4] — list_c is independent

4. Core Data Types

Python has several built-in data types. Understanding which type to use — and why — is a core skill.

4.1 int (Integer)

Whole numbers, positive or negative, with no size limit in Python 3.

Python
age = 25
year = 2026
big_number = 1_000_000  # underscores for readability (Python 3.6+)
negative = -42
zero = 0

# Integer literals in different bases
binary = 0b1010      # binary: 10
octal = 0o17         # octal: 15
hexadecimal = 0xFF   # hexadecimal: 255

print(binary, octal, hexadecimal)  # 10 15 255

# Integer operations
print(7 // 2)   # 3  — integer (floor) division
print(7 % 2)    # 1   modulo (remainder)
print(2 ** 10)  # 1024  exponentiation

# Python 3 integers have unlimited precision
factorial_20 = 1
for i in range(1, 21):
    factorial_20 *= i
print(factorial_20)  # 2432902008176640000 (exact, no overflow)

4.2 float (Floating Point)

Numbers with decimal points, following IEEE 754 double precision.

Python
pi = 3.14159
temperature = -12.5
scientific = 1.5e3    # 1500.0
small = 2.5e-4        # 0.00025

print(type(pi))       # <class 'float'>

# Float arithmetic precision issue (famous gotcha)
print(0.1 + 0.2)      # 0.30000000000000004 (NOT 0.3!)
print(0.1 + 0.2 == 0.3)  # False!

# Solution: use round() for comparison or decimal module
print(round(0.1 + 0.2, 2) == 0.3)  # True

# For financial calculations, use decimal.Decimal
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2"))  # 0.3 (exact)

# Special float values
import math
print(math.inf)       # inf
print(-math.inf)      # -inf
print(math.nan)       # nan (Not a Number)
print(math.isnan(math.nan))  # True

4.3 str (String)

Strings are immutable sequences of Unicode characters.

Python
# String creation  single, double, or triple quotes
name = 'Alice'
greeting = "Hello"
multiline = """This is
a multiline
string."""

# String with quotes inside
quote1 = "She said 'hello'"
quote2 = 'He replied "hi there"'
escaped = "It's a \"beautiful\" day"

# Raw strings  backslash is literal (useful for regex, Windows paths)
path = r"C:\Users\Alice\Documents"
pattern = r"\d+\.\d+"

print(len(name))        # 5
print(name[0])          # A
print(name[-1])         # e
print(name[1:4])        # lic (slicing)
print(name[::-1])       # ecilA (reverse)

4.4 bool (Boolean)

Only two values: True and False. In Python, bool is a subclass of int.

Python
is_active = True
is_deleted = False

print(type(is_active))    # <class 'bool'>
print(True + True)        # 2 (bool is subclass of int!)
print(True * 5)           # 5
print(False + 1)          # 1

# Boolean from comparisons
x = 10
print(x > 5)    # True
print(x == 5)   # False
print(x != 5)   # True

# Truthy and Falsy values
# Falsy: False, 0, 0.0, "", [], {}, set(), None
# Truthy: everything else
print(bool(0))      # False
print(bool(""))     # False
print(bool([]))     # False
print(bool(None))   # False
print(bool(1))      # True
print(bool("hi"))   # True
print(bool([0]))    # True (list with one item, even if item is falsy)

4.5 None

None represents the absence of a value. It is Python's null.

Python
result = None
print(result)           # None
print(type(result))     # <class 'NoneType'>

# None is a singleton — always use 'is' to compare, not '=='
if result is None:
    print("No result yet")

# Functions that don't return explicitly return None
def greet(name):
    print(f"Hello, {name}")

output = greet("Alice")  # prints: Hello, Alice
print(output)            # None

# None as a default parameter sentinel
def connect(host, port=None):
    if port is None:
        port = 443
    print(f"Connecting to {host}:{port}")

connect("api.example.com")         # Connecting to api.example.com:443
connect("api.example.com", 8080)   # Connecting to api.example.com:8080

5. type() and isinstance()

type()

Returns the exact type of an object.

Python
print(type(42))           # <class 'int'>
print(type(3.14))         # <class 'float'>
print(type("hello"))      # <class 'str'>
print(type(True))         # <class 'bool'>
print(type(None))         # <class 'NoneType'>
print(type([1, 2, 3]))    # <class 'list'>
print(type({"a": 1}))     # <class 'dict'>

# Using type() in conditions (usually avoid this pattern)
x = 42
if type(x) == int:
    print("x is an int")

isinstance()

Checks if an object is an instance of a class or any of its subclasses. Prefer this over type() for type checking.

Python
x = 42
print(isinstance(x, int))    # True
print(isinstance(x, float))  # False
print(isinstance(x, (int, float)))  # True  check multiple types

# Why isinstance is better: it respects inheritance
class Animal:
    pass

class Dog(Animal):
    pass

rex = Dog()
print(type(rex) == Animal)        # False  type() is exact
print(isinstance(rex, Animal))    # True  isinstance checks hierarchy
print(isinstance(rex, Dog))       # True

# Practical use: validate function inputs
def calculate_area(radius):
    if not isinstance(radius, (int, float)):
        raise TypeError(f"radius must be a number, got {type(radius).__name__}")
    if radius <= 0:
        raise ValueError("radius must be positive")
    return 3.14159 * radius ** 2

print(calculate_area(5))      # 78.53975
# calculate_area("big")       # TypeError: radius must be a number, got str

6. Type Casting

Converting between types explicitly.

int()

Python
# String to int
age = int("25")
print(age, type(age))   # 25 <class 'int'>

# Float to int  truncates (does NOT round)
x = int(3.9)
print(x)                # 3 (not 4!)
y = int(-3.9)
print(y)                # -3 (truncates toward zero)

# Bool to int
print(int(True))   # 1
print(int(False))  # 0

# Invalid conversions raise ValueError
# int("hello")     # ValueError: invalid literal for int()
# int("3.14")      # ValueError: float string needs float() first

# From other bases
print(int("0b1010", 2))   # 10 (binary string)
print(int("FF", 16))      # 255 (hex string)
print(int("17", 8))       # 15 (octal string)

float()

Python
# String to float
price = float("19.99")
print(price, type(price))  # 19.99 <class 'float'>

# Int to float
x = float(5)
print(x)    # 5.0

# Bool to float
print(float(True))   # 1.0
print(float(False))  # 0.0

# Special strings
print(float("inf"))   # inf
print(float("-inf"))  # -inf
print(float("nan"))   # nan

str()

Python
# Any type to string
print(str(42))          # "42"
print(str(3.14))        # "3.14"
print(str(True))        # "True"
print(str(None))        # "None"
print(str([1, 2, 3]))   # "[1, 2, 3]"

# Useful for string concatenation with non-strings
age = 30
# print("Age: " + age)         # TypeError!
print("Age: " + str(age))      # "Age: 30"
# Better: use f-string
print(f"Age: {age}")           # "Age: 30"

bool()

Python
# Explicit conversion
print(bool(0))      # False
print(bool(1))      # True
print(bool(-1))     # True (any non-zero)
print(bool(""))     # False
print(bool("  "))   # True (whitespace is truthy!)
print(bool([]))     # False
print(bool([0]))    # True

Real-world Type Conversion Example

Python
# Parsing user data from a form or CSV
raw_data = {
    "name": "Alice",
    "age": "28",
    "salary": "75000.50",
    "is_manager": "true",
    "department_id": "42"
}

parsed = {
    "name": raw_data["name"],
    "age": int(raw_data["age"]),
    "salary": float(raw_data["salary"]),
    "is_manager": raw_data["is_manager"].lower() == "true",
    "department_id": int(raw_data["department_id"])
}

print(parsed)
# {'name': 'Alice', 'age': 28, 'salary': 75000.5, 'is_manager': True, 'department_id': 42}

7. String Methods

Strings are objects in Python, and they come with a rich set of built-in methods. All string methods return a new string — strings are immutable.

Case Methods

Python
text = "Hello, World!"

print(text.upper())       # HELLO, WORLD!
print(text.lower())       # hello, world!
print(text.title())       # Hello, World!
print(text.capitalize())  # Hello, world! (only first char)
print(text.swapcase())    # hELLO, wORLD!

# Practical use: case-insensitive comparison
user_input = "YeS"
if user_input.lower() == "yes":
    print("User confirmed")

Whitespace Methods

Python
messy = "   hello world   "

print(repr(messy.strip()))   # 'hello world'    (both sides)
print(repr(messy.lstrip()))  # 'hello world   ' (left only)
print(repr(messy.rstrip()))  # '   hello world' (right only)

# Strip specific characters
url = "///path/to/resource///"
print(url.strip("/"))   # path/to/resource

csv_line = "Alice,28,Engineer\n"
print(csv_line.strip())  # Alice,28,Engineer

Search and Test Methods

Python
sentence = "The quick brown fox jumps over the lazy dog"

# Finding
print(sentence.find("fox"))      # 16 (index of first occurrence)
print(sentence.find("cat"))      # -1 (not found)
print(sentence.index("fox"))     # 16 (same but raises ValueError if not found)
print(sentence.count("the"))     # 1 (case sensitive)
print(sentence.count("the", 0, len(sentence)))  # 1

# Testing
print(sentence.startswith("The"))   # True
print(sentence.endswith("dog"))     # True
print(sentence.startswith(("The", "A")))  # True (tuple of options)

# Character testing
print("hello123".isalnum())    # True (letters and digits)
print("hello".isalpha())       # True (letters only)
print("12345".isdigit())       # True (digits only)
print("   ".isspace())         # True (whitespace only)
print("HELLO".isupper())       # True
print("hello".islower())       # True
print("Hello World".istitle()) # True

split() and join()

Python
# split()  string to list
sentence = "the quick brown fox"
words = sentence.split()         # split on whitespace
print(words)                     # ['the', 'quick', 'brown', 'fox']

csv_line = "Alice,28,New York,Engineer"
fields = csv_line.split(",")
print(fields)   # ['Alice', '28', 'New York', 'Engineer']

# Split with max splits
print("a:b:c:d".split(":", 2))  # ['a', 'b', 'c:d']

# splitlines() for multi-line text
poem = "Roses are red\nViolets are blue\nPython is great"
print(poem.splitlines())
# ['Roses are red', 'Violets are blue', 'Python is great']

# join()  list to string
words = ["the", "quick", "brown", "fox"]
print(" ".join(words))       # the quick brown fox
print("-".join(words))       # the-quick-brown-fox
print("".join(words))        # thequickbrownfox

# Common pattern: build CSV line
fields = ["Alice", "28", "Engineer"]
csv_line = ",".join(fields)
print(csv_line)  # Alice,28,Engineer

# Performance tip: always use join() to concatenate many strings
# SLOW: building a string with +=
result = ""
for i in range(1000):
    result += str(i)  # creates a new string each iteration!

# FAST: collect parts, then join
parts = []
for i in range(1000):
    parts.append(str(i))
result = "".join(parts)

replace()

Python
text = "I love cats. Cats are great. Cats rule!"

# Replace all occurrences
print(text.replace("Cats", "Dogs"))
# I love cats. Dogs are great. Dogs rule!

# Note: case-sensitive
print(text.replace("cats", "dogs"))
# I love dogs. Cats are great. Cats rule!

# Replace with count limit
print(text.replace("Cats", "Dogs", 1))
# I love cats. Dogs are great. Cats rule!

# Chaining replacements
dirty = "  Hello,   World!  "
clean = dirty.strip().replace(",", "").replace("  ", " ")
print(clean)  # Hello World!

format() and f-strings

Python
# str.format()  positional
print("Hello, {}! You are {} years old.".format("Alice", 28))

# str.format()  named
print("Name: {name}, Age: {age}".format(name="Alice", age=28))

# str.format()  formatting numbers
print("{:.2f}".format(3.14159))    # 3.14
print("{:,}".format(1000000))      # 1,000,000
print("{:>10}".format("hi"))       # "        hi" (right-align, width 10)
print("{:<10}".format("hi"))       # "hi        " (left-align)
print("{:^10}".format("hi"))       # "    hi    " (center)
print("{:0>5}".format("42"))       # "00042" (zero-pad)

# f-strings (Python 3.6+)  preferred modern approach
name = "Alice"
age = 28
score = 98.756

print(f"Name: {name}, Age: {age}")
print(f"Score: {score:.2f}")           # Score: 98.76
print(f"Double age: {age * 2}")        # Double age: 56
print(f"Uppercase: {name.upper()}")    # Uppercase: ALICE

# f-string debugging (Python 3.8+)
x = 42
print(f"{x=}")      # x=42
print(f"{x * 2=}")  # x * 2=84

# Multiline f-strings
user = {"name": "Alice", "city": "Oslo"}
message = (
    f"Dear {user['name']},\n"
    f"Welcome from {user['city']}!\n"
    f"Best regards"
)
print(message)

Other Useful String Methods

Python
# zfill()  zero-pad
print("42".zfill(5))     # 00042
print("042".zfill(5))    # 00042

# center(), ljust(), rjust()
print("hi".center(10))       # "    hi    "
print("hi".center(10, "*"))  # "****hi****"

# encode()  to bytes
text = "Hello"
encoded = text.encode("utf-8")
print(encoded)            # b'Hello'
print(encoded.decode())   # Hello

# in operator  membership test
print("fox" in "the quick brown fox")   # True
print("cat" in "the quick brown fox")   # False

8. Arithmetic Operators

Python
a = 17
b = 5

print(a + b)    # 22  addition
print(a - b)    # 12  subtraction
print(a * b)    # 85  multiplication
print(a / b)    # 3.4  true division (always returns float)
print(a // b)   # 3   — floor division (rounds down to integer)
print(a % b)    # 2    modulo (remainder)
print(a ** b)   # 1419857  exponentiation

# Integer division and modulo relationship
# a == (a // b) * b + (a % b)
print(17 == (17 // 5) * 5 + 17 % 5)  # True

# Modulo with negative numbers (Python follows mathematical convention)
print(-7 % 3)   # 2 (not -1!)  result has same sign as divisor
print(7 % -3)   # -2            result has same sign as divisor

# Floor division
print(-7 // 2)  # -4 (rounds toward negative infinity)
print(7 // 2)   # 3

# Operator precedence (PEMDAS / BODMAS)
print(2 + 3 * 4)        # 14 (not 20)
print((2 + 3) * 4)      # 20
print(2 ** 3 ** 2)      # 512 (right-to-left: 3**2=9, 2**9=512)
print((2 ** 3) ** 2)    # 64  (left-to-right)

# Augmented assignment operators
x = 10
x += 5   # x = x + 5   15
x -= 3   # x = x - 3   12
x *= 2   # x = x * 2   24
x //= 5  # x = x // 5 → 4
x **= 3  # x = x ** 3  64
x %= 10  # x = x % 10  4
print(x)  # 4

Math Module

Python
import math

print(math.sqrt(16))      # 4.0
print(math.ceil(3.2))     # 4 (round up)
print(math.floor(3.8))    # 3 (round down)
print(math.abs(-5))       # AttributeError!  use abs() built-in
print(abs(-5))            # 5
print(math.pow(2, 8))     # 256.0 (returns float)
print(2 ** 8)             # 256   (returns int)
print(math.log(100, 10))  # 2.0
print(math.log2(8))       # 3.0
print(math.pi)            # 3.141592653589793
print(math.e)             # 2.718281828459045

# Trigonometry (angles in radians)
print(math.sin(math.pi / 2))  # 1.0
print(math.cos(0))            # 1.0
print(math.degrees(math.pi))  # 180.0
print(math.radians(180))      # 3.141592653589793

9. Comparison Operators

Comparison operators return True or False.

Python
a = 10
b = 20

print(a == b)   # False  equal to
print(a != b)   # True   not equal to
print(a < b)    # True   less than
print(a > b)    # False  greater than
print(a <= b)   # True   less than or equal to
print(a >= b)   # False  greater than or equal to

# Chained comparisons (unique to Python)
x = 15
print(10 < x < 20)    # True  elegant!
print(0 <= x <= 100)  # True

# Equivalent to: 10 < x and x < 20
# But more readable and only evaluates x once

# Identity operators: is vs ==
# == checks VALUE equality
# is checks IDENTITY (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 objects
print(a is c)   # True   same object

# None comparison: always use 'is'
result = None
print(result == None)   # True (works but not recommended)
print(result is None)   # True (correct way)
print(result is not None)  # False

# Surprising: Python caches small integers (-5 to 256)
x = 256
y = 256
print(x is y)   # True (cached)

x = 257
y = 257
print(x is y)   # False (not cached  different objects)
# This is an implementation detail; don't rely on it

10. Logical Operators

and, or, and not combine boolean expressions.

Python
# and  True if both operands are True
print(True and True)    # True
print(True and False)   # False
print(False and True)   # False
print(False and False)  # False

# or  True if at least one operand is True
print(True or True)     # True
print(True or False)    # True
print(False or True)    # True
print(False or False)   # False

# not  inverts boolean
print(not True)   # False
print(not False)  # True

# Practical examples
age = 22
has_id = True
is_vip = False

# Check entry
if age >= 18 and has_id:
    print("Entry allowed")

# Check discount
if age < 25 or is_vip:
    print("Discount applies")

# Check NOT banned
is_banned = False
if not is_banned:
    print("User can log in")

Short-Circuit Evaluation

Python stops evaluating as soon as it can determine the result — this is called short-circuiting.

Python
# and: if first is False, skip second
def expensive_check():
    print("Running expensive check...")
    return True

result = False and expensive_check()  # expensive_check never called!
print(result)  # False

result = True and expensive_check()   # expensive_check IS called
print(result)  # True

# or: if first is True, skip second
result = True or expensive_check()    # expensive_check never called!
print(result)  # True

and/or Return Values (Not Just True/False)

This is a Python subtlety many beginners miss:

Python
# and returns first falsy value, or last value if all truthy
print(1 and 2)        # 2
print(0 and 2)        # 0
print("a" and "b")    # "b"
print("" and "b")     # ""

# or returns first truthy value, or last value if all falsy
print(1 or 2)         # 1
print(0 or 2)         # 2
print("" or "default") # "default"
print(None or [])     # []

# Practical idiom: default values
name = ""
display_name = name or "Anonymous"
print(display_name)   # Anonymous

config = None
settings = config or {}
print(settings)       # {}

11. A Complete Working Example

Let's combine everything into a small BMI calculator:

Python
def calculate_bmi():
    """BMI calculator demonstrating all covered concepts."""
    print("=" * 40)
    print("       BMI CALCULATOR")
    print("=" * 40)
    
    # Input with type conversion
    while True:
        try:
            weight_str = input("Enter your weight (kg): ").strip()
            weight = float(weight_str)
            if weight <= 0:
                print("Weight must be positive.")
                continue
            break
        except ValueError:
            print(f"'{weight_str}' is not a valid number. Try again.")
    
    while True:
        try:
            height_str = input("Enter your height (m): ").strip()
            height = float(height_str)
            if height <= 0:
                print("Height must be positive.")
                continue
            break
        except ValueError:
            print(f"'{height_str}' is not a valid number. Try again.")
    
    # Arithmetic
    bmi = weight / (height ** 2)
    
    # Comparison and logical operators
    if bmi < 18.5:
        category = "Underweight"
        advice = "Consider consulting a nutritionist."
    elif 18.5 <= bmi < 25.0:
        category = "Normal weight"
        advice = "Great! Keep it up."
    elif 25.0 <= bmi < 30.0:
        category = "Overweight"
        advice = "Consider more physical activity."
    else:
        category = "Obese"
        advice = "Please consult a healthcare professional."
    
    # String formatting
    print("\n" + "=" * 40)
    print(f"  Weight: {weight:.1f} kg")
    print(f"  Height: {height:.2f} m")
    print(f"  BMI:    {bmi:.1f}")
    print(f"  Status: {category}")
    print(f"  Advice: {advice}")
    print("=" * 40)
    
    # Boolean result
    is_healthy = 18.5 <= bmi < 25.0
    print(f"\nHealthy range: {is_healthy}")
    return bmi

# Uncomment to run interactively:
# calculate_bmi()

# For demonstration, test with known values
weight, height = 70.0, 1.75
bmi = weight / height ** 2
print(f"BMI for {weight}kg at {height}m: {bmi:.2f}")
# BMI for 70.0kg at 1.75m: 22.86

Key Takeaways

  1. Indentation is syntax — use 4 spaces consistently. Never mix tabs and spaces.
  2. Variables are references — mutable objects (lists, dicts) can be modified through any reference pointing to them.
  3. None is not zero or empty — always compare with is None, not == None.
  4. Floats are approximate — never use == to compare floats; use round() or math.isclose().
  5. isinstance() beats type() — it respects inheritance and is the Pythonic way to check types.
  6. Strings are immutable — all string methods return new strings. Assign the result.
  7. Use f-strings (Python 3.6+) — they are the most readable and fastest string formatting option.
  8. Short-circuit evaluation — use x or default as a concise way to provide fallback values.
  9. Type casting is explicit — Python will not implicitly convert "5" to 5. You must call int().
  10. and/or return values, not just booleans — learn this; it unlocks many Pythonic idioms.

Common Mistakes to Avoid

Python
# Mistake 1: Comparing None with ==
result = None
if result == None:  # Works but wrong style
    pass
if result is None:  # Correct
    pass

# Mistake 2: Modifying a string "in place"
name = "alice"
name.upper()         # Does nothing useful  returns new string
print(name)          # still "alice"
name = name.upper()  # Correct: assign the result
print(name)          # "ALICE"

# Mistake 3: Float equality
if 0.1 + 0.2 == 0.3:  # This is False!
    print("equal")
import math
if math.isclose(0.1 + 0.2, 0.3):  # Correct
    print("approximately equal")

# Mistake 4: Forgetting input() returns a string
age = input("Age: ")
# if age > 18:  # TypeError: '>' not supported between str and int
if int(age) > 18:  # Correct

# Mistake 5: Using 'is' for value comparison
a = 1000
b = 1000
if a is b:      # May be False (outside cached range)
    print("same")
if a == b:      # Correct for value comparison
    print("equal")  # True