No history yet

Python Logic Fundamentals

Writing Professional Python

Writing code is one thing; writing code that others can read and maintain is another. In the Python world, the community follows a style guide called . It's not about strict rules enforced by the compiler, but rather a set of conventions that lead to clean, readable code. Think of it as the grammar and etiquette of the Python language.

Following PEP 8 means using four spaces for indentation, limiting lines to 79 characters, and using clear, descriptive names for variables and functions. Adhering to these standards makes your code more professional and easier for you (and others) to work with later.

# Bad: Hard to read
def my_func(x,y,z):
    if x>y: return x*z-y
    else: return y*z-x

# Good: PEP 8 compliant
def calculate_value(main_value, threshold, factor):
    """Calculates a value based on a threshold."""
    if main_value > threshold:
        return (main_value * factor) - threshold
    else:
        return (threshold * factor) - main_value

Types and Mutability

Python is dynamically typed, meaning you don't have to declare a variable's type. The interpreter figures it out at runtime. This offers flexibility but can lead to unexpected errors if you're not careful. To help with this, modern Python supports type hinting, which allows you to annotate your code with expected types. These hints don't change how the code runs, but they are invaluable for static analysis tools and for other developers reading your code.

def greet(name: str) -> str:
    """Returns a greeting. Type hints show name is a string and the function returns a string."""
    return "Hello, " + name

# This still runs, but a type checker would flag it.
# greet(123)

Understanding the difference between mutable and immutable types is critical for avoiding subtle bugs. Immutable objects, like integers, strings, and tuples, cannot be changed after they are created. When you think you're modifying one, you're actually creating a new object in memory.

Mutable objects, such as lists, dictionaries, and sets, can be altered in place. This is more memory-efficient for large data structures but requires caution, especially when passing them into functions, as any modifications within the function will affect the original object.

Efficient Iteration

Loops are fundamental, but Python offers more expressive and often more efficient ways to iterate. List comprehensions provide a concise syntax for creating lists. They are often faster than appending to a list in a standard for loop because the memory allocation is handled more efficiently.

# Traditional for loop
squares = []
for i in range(10):
    squares.append(i**2)

# List comprehension (more Pythonic and efficient)
squares_comp = [i**2 for i in range(10)]

# You can also add conditional logic
even_squares = [i**2 for i in range(10) if i % 2 == 0]

For very large sequences, building a list in memory can be inefficient or impossible. This is where generator expressions shine. They look like list comprehensions but use parentheses instead of square brackets. Instead of creating a full list, they create a generator object that produces values one by one, on demand. This approach is called and is incredibly memory-friendly.

import sys

# A list comprehension stores all 100,000 numbers in memory
list_comp = [i for i in range(100000)]
print(f"List size: {sys.getsizeof(list_comp)} bytes")

# A generator expression creates a small generator object
gen_exp = (i for i in range(100000))
print(f"Generator size: {sys.getsizeof(gen_exp)} bytes")

# The values are produced only when you iterate over it
# for number in gen_exp:
#     print(number)

Handling Errors Gracefully

No matter how well you write your code, errors will happen. Network connections fail, files go missing, and users enter invalid data. Instead of letting your program crash, you can anticipate and manage these issues using exception handling.

The basic structure is a try...except block. You place the code that might fail inside the try block. If an error, or exception, occurs, the code in the except block is executed.

try:
    number = int(input("Enter a number: "))
    result = 10 / number
    print(f"The result is {result}")
except ValueError:
    print("That wasn't a valid number!")
except ZeroDivisionError:
    print("You can't divide by zero!")

For more control, you can add else and finally clauses. The else block runs only if the try block completes without raising an exception. This is useful for separating your success-case logic from your main try block.

The finally block is special: it runs no matter what. It executes whether an exception occurred, was handled, or not. This makes it the perfect place for cleanup actions, like closing a file or releasing a network connection, ensuring your program leaves things in a tidy state.

f = None # Define f before the try block
try:
    f = open('my_file.txt', 'r')
    content = f.read()
except FileNotFoundError:
    print("Could not find the file.")
else:
    print("File read successfully:", content)
finally:
    # This will run whether the file was found or not
    if f:
        print("Closing the file.")
        f.close()

This structure gives you a robust pattern for writing resilient code that can handle unexpected situations without crashing.

Quiz Questions 1/6

What is the primary purpose of PEP 8?

Quiz Questions 2/6

In Python's exception handling, when is the code inside a finally block executed?