Python Syntax and Data Types

The foundation of Python programming

Introduction to Python

Python is a high-level, interpreted programming language known for its readability, simplicity, and versatility. Created by Guido van Rossum and first released in 1991, Python has grown to become one of the world's most popular programming languages, especially in web development, data science, machine learning, and automation.

Python's philosophy emphasizes code readability, with its notable use of significant whitespace (indentation) to delimit code blocks rather than curly braces or keywords. This enforced indentation creates visually clean code that is easy to understand and maintain.

timeline title Python Evolution 1991 : Python 1.0 : Created by Guido van Rossum 2000 : Python 2.0 : List comprehensions : Garbage collection 2008 : Python 3.0 : Major revision : Unicode strings 2016 : Python 3.6 : f-strings : Type hints 2020 : Python 3.9 : Dictionary merging 2023 : Python 3.12 : Performance improvements : Enhanced error messages

Python's design philosophy is captured in the "Zen of Python" (accessible by typing import this in the Python interpreter):

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Readability counts.

This philosophy guides how Python code is written and structured, emphasizing clarity and readability over brevity or cleverness.

Think of Python as a versatile toolbox with well-organized, clearly labeled tools. Even if you're new to carpentry (programming), you can quickly find and use what you need without specialized knowledge.

Basic Syntax Elements

Python's syntax is designed to be clean and readable. Here are the fundamental elements:

Statements and Whitespace

Unlike many languages that use semicolons to terminate statements and braces to group code blocks, Python uses:

# Examples of Python statements and indentation
name = "Alice"  # A simple assignment statement

# A conditional block using indentation
if name == "Alice":
    print("Hello, Alice!")  # Indented block
    print("Welcome back.")  # Still in the indented block
else:
    print("Hello, stranger!")  # Different indented block

# This line is outside the conditional block
print("End of program.")

Comments

Python supports single-line comments beginning with the # character, and multi-line comments using triple quotes:

# This is a single-line comment

"""
This is a multi-line comment or docstring.
It can span multiple lines and is often used
for documentation.
"""

Variables and Assignment

Python uses a simple assignment operator = to assign values to variables. Variables don't need to be declared with types; Python determines the type automatically based on the assigned value:

# Variable assignment
name = "Bob"       # A string
age = 25           # An integer
height = 1.85      # A floating-point number
is_student = True  # A boolean

# Multiple assignment
x, y, z = 1, 2, 3

# Value swapping (without a temporary variable)
a, b = 5, 10
a, b = b, a  # a is now 10, b is now 5

Python Naming Conventions

These conventions aren't enforced by the interpreter but are widely followed as part of Python's culture of readability.

Python's syntax is like well-structured writing with clear paragraphs and headings. The indentation visually reinforces the code's logical structure, making it easier to understand the flow and relationships between different parts.

Built-in Data Types

Python comes with several built-in data types that are immediately available for use:

Numeric Types

# Numeric type examples
a = 42       # int
b = 3.14159  # float
c = 2 + 3j   # complex

# Type conversion
float_a = float(a)  # 42.0
int_b = int(b)      # 3 (truncates, doesn't round)
abs_c = abs(c)      # 3.605551275463989 (magnitude)

Boolean Type

The bool type has only two values: True and False. Note the capitalization, which is different from many other languages.

# Boolean examples
is_valid = True
has_error = False

# Boolean conversion
print(bool(0))     # False
print(bool(42))    # True
print(bool(""))    # False
print(bool("Hi"))  # True
print(bool([]))    # False
print(bool([1,2])) # True

Sequence Types

# String examples
greeting = "Hello, World!"
name = 'Alice'
multiline = """This is a
multiline string."""

# List examples (mutable)
fruits = ["apple", "banana", "cherry"]
mixed = [1, "hello", 3.14, True]
fruits.append("orange")  # Add an item
fruits[0] = "pear"       # Modify an item

# Tuple examples (immutable)
coordinates = (10, 20)
person = ("Bob", 25, "Engineer")
# coordinates[0] = 5  # This would raise an error

# Range example
numbers = range(1, 10, 2)  # 1, 3, 5, 7, 9
for num in numbers:
    print(num)

Mapping Type

# Dictionary examples
person = {
    "name": "Charlie",
    "age": 30,
    "profession": "Developer"
}

# Accessing dictionary values
print(person["name"])      # Charlie
person["age"] = 31         # Update a value
person["location"] = "NYC" # Add a new key-value pair

# Using get() with a default value
email = person.get("email", "Not provided")  # "Not provided"

Set Types

# Set examples
colors = {"red", "green", "blue"}
colors.add("yellow")  # Add an item
colors.add("red")     # Adding duplicate has no effect

# Set operations
primary = {"red", "yellow", "blue"}
secondary = {"green", "purple", "orange"}

all_colors = primary | secondary  # Union
common = primary & secondary      # Intersection
difference = primary - secondary  # Difference

None Type

Python has a special None type to represent the absence of a value or a null value:

# None example
result = None

def function_without_return():
    print("This function doesn't return anything")

x = function_without_return()  # x will be None

Python's built-in data types are like different types of containers in a kitchen. Strings are like recipe cards with text, lists are like adjustable spice racks where you can add or remove items, tuples are like sealed food packages that can't be changed once created, and dictionaries are like labeled storage bins where you can quickly find items by their names.

Type Checking and Conversion

Python is dynamically typed, meaning variable types are determined at runtime. However, you can check a variable's type and convert between types as needed:

Checking Types

# Using the type() function
name = "David"
age = 40
print(type(name))  # <class 'str'>
print(type(age))   # <class 'int'>

# Using isinstance() for type checking
print(isinstance(name, str))    # True
print(isinstance(age, float))   # False
print(isinstance(age, (int, float)))  # True - is either int or float

Type Conversion (Casting)

# Converting between types
age_str = "42"
age_int = int(age_str)  # Convert string to integer
age_float = float(age_int)  # Convert integer to float
back_to_str = str(age_float)  # Convert float to string

# List/tuple/set conversion
my_list = [1, 2, 3, 2, 1]
my_tuple = tuple(my_list)  # (1, 2, 3, 2, 1)
my_set = set(my_list)      # {1, 2, 3} - duplicates removed

# Dictionary conversion
items = [("a", 1), ("b", 2)]
my_dict = dict(items)  # {'a': 1, 'b': 2}
graph TD A[Original Value] --> B{Can be converted?} B -->|Yes| C[New Type] B -->|No| D[TypeError] E[str '123'] -->|int()| F[int 123] G[str 'hello'] -->|int()| H[ValueError] I[int 456] -->|str()| J[str '456'] K[list [1,2,3]] -->|tuple()| L[tuple (1,2,3)]

It's important to note that not all conversions are possible, and attempting an invalid conversion will result in a ValueError or TypeError.

Type conversion in Python is like currency exchange. Just as you can convert dollars to euros (with some rules and potential loss of precision), you can convert between data types, but you need to be aware of the conversion rules and potential data loss.

String Operations and Formatting

Strings are one of the most commonly used data types in Python, and the language provides extensive capabilities for working with them:

String Concatenation and Repetition

# String concatenation with +
first_name = "John"
last_name = "Doe"
full_name = first_name + " " + last_name  # "John Doe"

# String repetition with *
divider = "-" * 20  # "--------------------"

String Indexing and Slicing

# Indexing (0-based)
text = "Python"
first_char = text[0]   # 'P'
last_char = text[-1]   # 'n'

# Slicing [start:end:step]
substring = text[1:4]    # 'yth' (from index 1 to 3)
reversed_text = text[::-1]  # 'nohtyP' (reverse)
every_other = text[::2]     # 'Pto' (every 2nd character)

Common String Methods

message = "   Hello, World!   "

# Case methods
print(message.upper())       # "   HELLO, WORLD!   "
print(message.lower())       # "   hello, world!   "
print(message.capitalize())  # "   hello, world!   "
print(message.title())       # "   Hello, World!   "

# Whitespace methods
print(message.strip())       # "Hello, World!"
print(message.lstrip())      # "Hello, World!   "
print(message.rstrip())      # "   Hello, World!"

# Search methods
print(message.find("World"))  # 9
print("World" in message)     # True
print(message.count("l"))     # 3

# Replace method
print(message.replace("World", "Python"))  # "   Hello, Python!   "

# Split and join
words = message.strip().split(", ")  # ["Hello", "World!"]
new_message = ", ".join(words)       # "Hello, World!"

String Formatting

Python offers several ways to format strings:

Format Method

# Using the format() method
name = "Eve"
age = 28
message = "My name is {} and I am {} years old.".format(name, age)

# With named placeholders
message = "My name is {name} and I am {age} years old.".format(name=name, age=age)

# With positional index
message = "My name is {0} and I am {1} years old.".format(name, age)

F-Strings (Formatted String Literals)

# Using f-strings (Python 3.6+)
name = "Frank"
age = 35
message = f"My name is {name} and I am {age} years old."

# With expressions
message = f"In 5 years, {name} will be {age + 5} years old."

# With formatting specifications
pi = 3.14159265359
formatted = f"Pi rounded to 2 decimal places: {pi:.2f}"  # 3.14

Old-style % Formatting (still in use)

# Old-style formatting (similar to C printf)
name = "Grace"
age = 42
message = "My name is %s and I am %d years old." % (name, age)

String operations in Python are like text editing tools. You can cut (slice), glue (concatenate), find and replace text, adjust formatting (case), and even create templates with placeholders (formatting) – all the tools you'd expect in a modern word processor.

Container Operations

Python's container types (lists, tuples, dictionaries, sets) share many common operations:

Length and Membership

# Length with len()
fruits = ["apple", "banana", "cherry"]
print(len(fruits))  # 3

# Membership testing with in
print("apple" in fruits)  # True
print("mango" in fruits)  # False

Iteration

# Iterating over a list
for fruit in fruits:
    print(fruit)

# Iterating with index
for i, fruit in enumerate(fruits):
    print(f"{i}: {fruit}")

# Iterating over a dictionary
person = {"name": "Helen", "age": 24, "city": "Boston"}
for key in person:
    print(f"{key}: {person[key]}")

# Iterating over key-value pairs
for key, value in person.items():
    print(f"{key}: {value}")

Common List Operations

numbers = [1, 2, 3, 4, 5]

# Adding elements
numbers.append(6)       # [1, 2, 3, 4, 5, 6]
numbers.insert(0, 0)    # [0, 1, 2, 3, 4, 5, 6]
numbers.extend([7, 8])  # [0, 1, 2, 3, 4, 5, 6, 7, 8]

# Removing elements
numbers.remove(0)    # [1, 2, 3, 4, 5, 6, 7, 8]
popped = numbers.pop()  # popped = 8, numbers = [1, 2, 3, 4, 5, 6, 7]
popped_index = numbers.pop(0)  # popped_index = 1, numbers = [2, 3, 4, 5, 6, 7]

# Finding elements
index = numbers.index(5)  # 3
count = numbers.count(2)  # 1

# Sorting and reversing
numbers.sort()          # [2, 3, 4, 5, 6, 7]
numbers.sort(reverse=True)  # [7, 6, 5, 4, 3, 2]
numbers.reverse()       # [2, 3, 4, 5, 6, 7]

Common Dictionary Operations

person = {"name": "Ian", "age": 31}

# Adding or updating entries
person["city"] = "Chicago"  # Add new key-value pair
person["age"] = 32          # Update existing value

# Getting values
name = person["name"]        # Direct access (raises KeyError if not found)
city = person.get("city")    # Using get() method
email = person.get("email", "Not available")  # With default value

# Removing entries
age = person.pop("age")      # Remove and return the value
del person["city"]           # Remove without returning

# Other operations
keys = list(person.keys())    # Get all keys
values = list(person.values())  # Get all values
items = list(person.items())  # Get all key-value pairs
person.update({"email": "ian@example.com", "phone": "555-1234"})  # Bulk update

These container operations provide the building blocks for data manipulation in Python. They're like the common tools used across different types of storage systems – ways to add, remove, find, and organize items regardless of whether they're in a list, a dictionary, or another container type.

List and Dictionary Comprehensions

Python offers concise ways to create lists and dictionaries using comprehensions:

List Comprehensions

# Creating a list of squares using a loop
squares = []
for x in range(10):
    squares.append(x ** 2)
print(squares)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# The same with a list comprehension
squares = [x ** 2 for x in range(10)]
print(squares)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# List comprehension with a condition
even_squares = [x ** 2 for x in range(10) if x % 2 == 0]
print(even_squares)  # [0, 4, 16, 36, 64]

# Nested list comprehension
matrix = [[i * j for j in range(1, 4)] for i in range(1, 4)]
print(matrix)  # [[1, 2, 3], [2, 4, 6], [3, 6, 9]]

Dictionary Comprehensions

# Creating a dictionary with a loop
square_dict = {}
for x in range(5):
    square_dict[x] = x ** 2
print(square_dict)  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# The same with a dictionary comprehension
square_dict = {x: x ** 2 for x in range(5)}
print(square_dict)  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Dictionary comprehension with a condition
even_square_dict = {x: x ** 2 for x in range(10) if x % 2 == 0}
print(even_square_dict)  # {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

# Creating a dictionary from two lists
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
name_age_dict = {name: age for name, age in zip(names, ages)}
print(name_age_dict)  # {'Alice': 25, 'Bob': 30, 'Charlie': 35}

Set Comprehensions

# Creating a set with a loop
squares_set = set()
for x in range(5):
    squares_set.add(x ** 2)
print(squares_set)  # {0, 1, 4, 9, 16}

# The same with a set comprehension
squares_set = {x ** 2 for x in range(5)}
print(squares_set)  # {0, 1, 4, 9, 16}

# Get unique letters from a string
text = "hello world"
letters = {char for char in text if char.isalpha()}
print(letters)  # {'e', 'h', 'd', 'l', 'o', 'r', 'w'}

Comprehensions are a powerful and concise way to create collections. They're like specialized manufacturing machines that can quickly produce a collection of items according to your specifications, often replacing several lines of more verbose loop code with a single, readable line.

graph LR A[Input Sequence] -->|for each element| B[Transformation] B -->|if condition| C[Output Collection] D[range(10)] -->|for x in range(10)| E[x ** 2] E -->|if x % 2 == 0| F[List of even squares]

Advanced Data Types

In addition to the basic data types, Python includes several more specialized types in the standard library:

Named Tuples

Named tuples are like tuples but with field names, making your code more readable:

from collections import namedtuple

# Define a named tuple type
Point = namedtuple('Point', ['x', 'y'])

# Create instances
p1 = Point(1, 2)
p2 = Point(x=3, y=4)

# Access by name or position
print(p1.x, p1.y)  # 1 2
print(p1[0], p1[1])  # 1 2

# Immutable like regular tuples
# p1.x = 5  # This would raise an AttributeError

Default Dictionaries

Default dictionaries automatically create missing keys with a default value:

from collections import defaultdict

# Create a defaultdict with int as default factory
word_counts = defaultdict(int)

# Count words in a text
text = "apple banana apple cherry banana apple"
for word in text.split():
    word_counts[word] += 1  # No KeyError for new words

print(dict(word_counts))  # {'apple': 3, 'banana': 2, 'cherry': 1}

# defaultdict with list as default factory
grouped_names = defaultdict(list)
people = [("Alice", 25), ("Bob", 25), ("Charlie", 30), ("David", 30)]

for name, age in people:
    grouped_names[age].append(name)

print(dict(grouped_names))  # {25: ['Alice', 'Bob'], 30: ['Charlie', 'David']}

Ordered Dictionaries

Ordered dictionaries maintain the insertion order of keys (note: in Python 3.7+, regular dictionaries also maintain order):

from collections import OrderedDict

# Create an ordered dictionary
od = OrderedDict()
od['first'] = 1
od['second'] = 2
od['third'] = 3

# Items are retrieved in insertion order
for key, value in od.items():
    print(key, value)

Counter

Counter is a specialized dictionary for counting hashable objects:

from collections import Counter

# Count elements in a list
fruits = ['apple', 'orange', 'apple', 'banana', 'orange', 'apple']
fruit_counter = Counter(fruits)
print(fruit_counter)  # Counter({'apple': 3, 'orange': 2, 'banana': 1})

# Most common elements
print(fruit_counter.most_common(2))  # [('apple', 3), ('orange', 2)]

# Count characters in a string
char_counter = Counter("Mississippi")
print(char_counter)  # Counter({'i': 4, 's': 4, 'p': 2, 'M': 1})

These advanced data types are like specialized tools designed for specific tasks. While the basic data types are like general-purpose tools that can handle many jobs, these specialized types are optimized for particular scenarios, making your code more efficient and readable when dealing with those specific tasks.

Practice Activities

Activity 1: Data Type Explorer

Create a Python script that:

  1. Defines variables of each basic data type (int, float, string, list, tuple, dictionary, set)
  2. Prints the type and value of each variable
  3. Attempts to convert each type to every other type and handles any errors
  4. Demonstrates at least three operations specific to each data type

Activity 2: String Manipulator

Write a program that:

  1. Takes a user input string
  2. Counts the number of vowels and consonants
  3. Reverses the string
  4. Creates an acronym from the first letter of each word
  5. Checks if the string is a palindrome (reads the same forwards and backwards)

Activity 3: Data Processing Challenge

Given this data structure:

data = [
    {"name": "Alice", "age": 25, "city": "New York"},
    {"name": "Bob", "age": 30, "city": "Chicago"},
    {"name": "Charlie", "age": 35, "city": "New York"},
    {"name": "David", "age": 25, "city": "Boston"},
    {"name": "Eve", "age": 30, "city": "Chicago"}
]

Write code to:

  1. Find all people from a specific city
  2. Calculate the average age
  3. Group people by age
  4. Sort people by name
  5. Create a new list with just names and cities using a list comprehension

Key Takeaways

In our next lecture, we'll explore control structures in Python, including conditional statements, loops, and functions, which will allow us to add logic and reusable behavior to our programs.