Python Annotation Scopes

So, what are these “annotation scopes” I’m talking about? Well, they’re those little things in your code that look like this: `def`, `class`, and `function`. They might seem harmless at first glance, but trust me when I say they can be a real pain to deal with.

Let’s start with the most common annotation scope the function. This is where you define what your code will do. For example:

# This is a function that takes in two parameters, x and y, and returns the sum of the two numbers.
def add_numbers(x: int, y: int) -> int:
    # The function is annotated with the types of the parameters and the return value.
    # This helps with type checking and makes the code more readable.
    return x + y
    # The function simply adds the two numbers and returns the result.

This function takes two numbers as input and returns their sum. Pretty straightforward, right? But what happens if we try to use this function outside of its scope? Let’s see:

# This function takes two numbers as input and returns their sum.
def add_numbers(x, y): # Function definition with two parameters x and y
    return x + y # Returns the sum of x and y

# This won't work!
add_numbers(3, 5) # Calling the function with arguments 3 and 5
# TypeError: 'function' object is not callable

# The issue here is that the function is being called before it is defined.
# To fix this, we need to define the function before calling it.

# Function definition
def add_numbers(x, y):
    return x + y

# Calling the function with arguments 3 and 5
add_numbers(3, 5) # Output: 8

Oops. Looks like our function isn’t defined yet. We need to make sure it’s inside a scope that can be called. In this case, we want to put the `add_numbers()` function in its own file or module so we can import and use it elsewhere:

# my_math.py
# This is a script that defines a function called "add_numbers" which takes in two parameters and returns their sum.

def add_numbers(x, y): # This line defines the function "add_numbers" and specifies its parameters, x and y.
    return x + y # This line returns the sum of the two parameters, x and y.

Now let’s say you want to define a class that will do something cool. This is where the `class` annotation scope comes in handy:

# Defining a class called MyClass
class MyClass:
    # Defining a constructor method with a parameter called name
    def __init__(self, name):
        # Assigning the value of the parameter name to the attribute name of the class
        self.name = name
    
    # Defining a method called print_name
    def print_name(self):
        # Printing a string with the value of the attribute name
        print("My name is:", self.name)

This class has a constructor (`__init__`) that sets the `name` attribute and a method (`print_name`) that prints out your name when called. But again, we need to make sure this class is defined in its own scope:

# my_class.py
class MyClass: # defining a class named MyClass
    def __init__(self, name): # defining a constructor method with self and name as parameters
        self.name = name # setting the name attribute of the class to the value passed in as the name parameter
    
    def print_name(self): # defining a method named print_name with self as a parameter
        print("My name is:", self.name) # printing out a string and the value of the name attribute of the class when the method is called

Now let’s say you want to define a function inside another function. This is where the `function` annotation scope comes in handy:

# Defining a function called "outer"
def outer():
    # Defining a function called "inner" inside the "outer" function
    def inner():
        # Do something here
        print("This is the inner function")
    
    # Calling the "inner" function within the "outer" function
    return inner()

# Calling the "outer" function
outer()

# Output:
# This is the inner function

This function, called `outer`, defines another function (called `inner`) inside its own scope. When we call the `outer` function, it returns the `inner` function which can then be used elsewhere:

# Defining the outer function
def outer():
    # Defining the inner function
    def inner():
        # Printing "Hello!" to the console
        print("Hello!")
    # Returning the inner function
    return inner

# Calling the outer function and then the inner function
print(outer()()) # Output: Hello!

Remember, always keep your functions inside their own scope (or else they’ll be unhappy) and make sure your classes are defined in their own file or module so you can import them elsewhere.

SICORPS