Robert Johns | 23 Oct, 2023

10 Python Concepts I Wish I Knew Earlier For Interviews [2024]

In this article, we share the 10 Python concepts I wish I knew earlier for acing interviews in 2024, including detailed explanations of these Python concepts, code examples, and interview questions that use these Python concepts.

As we move into the latter part of 2024, Python is still a top 3 language with huge demand in data science, web development, and more.

And with the Bureau of Labor and Statistics reporting an average salary of over $100K for programmers, learning essential Python concepts to land a job can be highly rewarding.

Even if you’re new to the Python job market or are unsure what to expect during an interview, we’ve taken the time to cover the technical interview process and how learning these Python concepts can help you succeed and stand out from the crowd.

Let’s dive in!

How To Succeed In Python Interviews?

Let’s set the scene. You’ve landed an interview with a company that you really want to work for. Congrats by the way! But how can you give yourself the best chance of acing that interview and landing that Python job?

Of course, you should aim to have a portfolio of Python projects while also refreshing computer science fundamentals and Python basics, but how can you stand out from the crowd?

The answer is simple: whether you're on a phone screener or under the spotlight in an on-site interview, the depth and breadth of your Python knowledge can be the difference maker in helping you to stand out and leave a positive impression on your interviewer. 

This is the whole reason for writing this article: to cover 10 Python concepts I wish I knew earlier when attending Python interviews. 

Trust me, you don’t want to leave an interview thinking you should have spent more time learning Python concepts the employer actually wants you to use. Been there, and it’s not a great feeling!

Quick disclaimer: this isn't just about knowing syntax but about understanding Python concepts that show up in coding challenges and conceptual interview questions.

So whether you’re fresh out of college or you’ve just finished a Python course, having a grasp of these Python concepts will demonstrate your technical prowess and also boost your ability to skillfully use Python. Sounds like a win-win!

I’ve also listed these Python concepts in order of difficulty. So, depending on your appetite for stretching yourself, take your time and work your way through to level-up your skills. 

But before we dive into the 10 Python concepts I wish I knew earlier, let’s take a quick detour to cover the technical interview process (feel free to skip ahead if this isn’t your first rodeo!).

The Technical Interview Process

If this is your first Python interview, you might be unsure what to expect. So, let’s take a few moments to explore the technical interview, including the two types of technical interviews.

First off, we should say that a technical interview is designed to evaluate your problem-solving skills, coding ability, and understanding of algorithmic concepts. 

The idea is to give interviewers a lens into how you think, code, and optimize solutions, often under pressure.

Let’s now dig into the two phases of technical interviews, namely the phone interview and the on-site interview.

Phone interviews, or screeners, are typically the initial step in the interview process. These gauge whether you possess the fundamental skills and knowledge to progress to the more intensive rounds.

In general, these can range from 30 minutes to an hour, and they can involve:

  • Coding on shared platforms so the interviewer can see your code in real-time
  • Discussing your past projects, focusing on your contribution and the technologies used
  • Basic to intermediate coding challenges that focus on algorithms and data structures
  • Answering theoretical questions related to the language, which in this case, is Python

After breezing through that first round, the next step is the on-site interview. 

On-site interviews are usually made up of multiple rounds, each focusing on various aspects of your tech skills. Naturally, these will be tailored to the type of role you are applying for, but some of the things you might see are:

  • Deep-dive coding challenges that require you to write, debug, and optimize code on a whiteboard or computer.
  • Design interviews, where you are asked to design complex systems or services and to showcase your systems and architecture knowledge.
  • Behavioral rounds to understand your team dynamics, culture fit, and past experiences.
  • Specialized rounds focused on technologies or areas that are important for the role, like data analysis or web development.

By knowing what to expect in a Python interview, you immediately increase your chances of success, so if you’ve made it this far, you’re already on the right path.

And even if you’re still new to Python or are only starting out, it’s a great idea to be prepared so that when you apply for jobs, it’s not a shock. 

This is also why we’ve designed our Python course to dive deep into the Python language and to help you think like a Python pro so that when you get to an interview, you’re ready to go!

So, without further ado, let’s dive into 10 Python concepts to help you ace your next Python interview.

 

1. List Comprehensions

We all use Python lists a lot. But list comprehensions provide a concise way to create lists in Python rather than using a for loop. 

This Python concept can take a little getting used to, but once you get to grips with list comprehensions, they’re a more Pythonic and readable way to generate lists than with traditional loops.

Some would even say that using a for loop over a list comprehension is a common Python mistake, so let’s try not to make it!

Let’s take a look at the general syntax for list comprehensions. The skill here is to read this in different segments, starting with the for loop segment. You’ll see this is identical to a standard for loop but without a colon.

We then move on to the optional conditional statement, which simply checks whether the current loop variable meets the conditional criteria. Finally, we pass the loop variable to the expression for processing, and the results are then appended to a list.

Also, notice that the entire list comprehension syntax is enclosed in square brackets, as the result of each iteration is appended to a list. We can then assign or process this list after it’s returned. 

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
List Comprehensions: General Syntax
'''
[expression for item in iterable if condition]

Let’s look at a classic use case for a list comprehension, which is when we want to find the squares of all even numbers in a list. We’ll also compare this to a standard for loop to show that the list comprehension is a far more elegant solution, as shown below.

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
List Comprehension Example
'''
# Traditional for loop
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
squares_of_evens = []

for num in numbers:
  if num % 2 == 0:
      squares_of_evens.append(num**2)

# List comprehension
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
squares_of_evens = [num**2 for num in numbers if num % 2 == 0]

Great, let’s take a look at two common interview questions that use list comprehensions.

Pro tip: if you're using an AI coding assistant like GitHub Copilot or Amazon CodeWhisperer, you could use this to generate code answers for more questions using list comprehensions. Just write out the question as a comment, and voila! This can be a great way to test yourself further. 

Question: Given a list of words, can you generate a new list containing the lengths of each word, but only for words greater than three characters long?

Answer Code:

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
List Comprehension Interview Question
'''
words = ['apple', 'banana', 'kiwi', 'grape']
lengths_of_words = [len(word) for word in words if len(word) > 3]

# Result: [5, 6, 5]

Question: Given a matrix (a list of lists), can you flatten it into a single list using list comprehensions?

Answer Code:

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
List Comprehension Interview Question
'''
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened_list = [number for row in matrix for number in row]

# Result: [1, 2, 3, 4, 5, 6, 7, 8, 9]

2. Generators

These are a type of iterable that can save memory, make code more readable, and are frequently encountered when dealing with lazy evaluation in Python. Pick up any good Python book, and you’ll find a section on generators for lazy evaluation.

Lazy evaluation, as the name suggests, means evaluating a part of a program or expression only when it's needed rather than upfront. Unlike a list, which computes all values upfront and stores them in memory, generators compute values on the fly. 

So, rather than computing every possible value and storing them, you compute values one by one and only when they are requested. This makes them memory-efficient, especially for large datasets or sequences, which is pretty cool.

To create a generator, you create a function and use the yield keyword. When a function uses this, it doesn't return a single value, but it returns a generator object that can be iterated over.

Let’s look at a simple example of how to create a generator object, as shown below.

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Generator: General Example
'''
def number_generator(limit):
  num = 0
  while num < limit:
      yield num
      num += 1

gen = number_generator(5)
for number in gen:
  print(number)
# Result: 0, 1, 2, 3, 4

In this example, we’ve created a generator function that takes one argument that sets the exit condition for a while loop.

When we enter the while loop, the generator function uses lazy loading to yield the current value of the num variable before incrementing it by 1. This continues until the limit is reached and the while loop exits.

To use the generator, we call the function and assign the generator object to the gen variable. We can then iterate over this generator object with a for loop.

This is the magic of the generator, as for each iteration, Python fetches the next value from the generator. The generator then resumes execution until it hits the yield statement again, signaling it to provide the yielded value, causing it to pause again.

This process repeats until the generator is exhausted, which is when the while condition in the generator is False.

Another way that we could handle this example involves using the built-in next function, as shown below. 

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Generator: General Example
'''
def number_generator(limit):
    num = 0
    while num < limit:
        yield num
        num += 1

gen = number_generator(5)
limit = 5

for _ in range(limit):
    print(next(gen))
# Result: 0, 1, 2, 3, 4

The Python next function is closely associated with generators, as it's used to retrieve the next item from a generator object. 

Behind the scenes, the next function continues to poll the generator for the next return value until there is nothing left to return. At this point, it would return a StopIteration exception. 

However, we can avoid this exception by using a for loop with a defined stop limit, as shown above.

Here, we're using our prior knowledge of the number of items to be generated to set an explicit loop boundary. This means we will never go beyond the generator's limit, preventing a StopIteration exception from being raised.

So, by using the next function within the for loop, the generator returns a value whenever it encounters the yield keyword, after which the next function causes it to run again. This process repeats until the exit criteria are met.

Let’s now take a look at a typical interview scenario involving generators:

Question: How can you create a generator that yields infinite Fibonacci numbers?

Answer Code:

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Generator: Interview Question
'''
def fibonacci_generator():
  a, b = 0, 1
  while True:
      yield a
      a, b = b, a + b

# Using the generator to get the first 5 Fibonacci numbers
gen = fibonacci_generator()
for _ in range(5):
  print(next(gen))
 
# Result: 0, 1, 1, 2, 3

3. Decorators

Python decorators are powerful tools that you can use to modify or extend existing functions or methods without permanently altering the original function. At their core, decorators are essentially wrappers around functions.

When we talk about wrapping a function, we mean taking the original function and adding some functionality before and/or after it. The decorator defines this extra functionality.

To create a decorator, it helps to think of it as a function that takes another function as an argument and returns a new function that extends or alters the behavior of the original function.

Let's fire up our Python IDE and dive into a simple example to understand the basic structure and usage of a decorator, as shown below.

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Decorators: General Example
'''
def simple_decorator(func):
  def wrapper():
      print("Something is happening before the function is called.")
      func()
      print("Something is happening after the function is called.")
  return wrapper

@simple_decorator
def say_hello():
  print("Hello!")

say_hello()
# Result:
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.

In this example, the simple_decorator function takes another function as an argument and defines a new function called wrapper within the body of simple_decorator. We then use the func argument to call this function within the wrapper function. 

Importantly, notice that the wrapper function adds behavior before and after calling the original function, func.

The final step is to create another function called say_hello, allowing us to use the @ syntax to apply the decorator to the say_hello function.

When we call say_hello, we see that the wrapper function is executed, which leads to the additional print statements both before and after the Hello! Message that is printed by the say_hello function.

The TL-DR here is that we’ve created a decorator as a wrapper function that takes in another function as an argument. This allows us to modify the original function's behavior by applying the decorator via the @syntax.

Let's take a look at a typical interview scenario involving decorators:

Question: How can you create a decorator that measures the time taken by a function to execute?

Answer Code:

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Decorators: Interview Question
'''
import time

def timer_decorator(func):
  def wrapper(*args, **kwargs):
      start_time = time.time()
      result = func(*args, **kwargs)
      end_time = time.time()
      print(f"{func.__name__} executed in {end_time - start_time} seconds.")
      return result
  return wrapper

@timer_decorator
def some_function(duration):
  time.sleep(duration)

some_function(2)
# Result: some_function executed in approximately 2 seconds.

4. Lambda Functions

Lambda functions are ideal for writing concise, one-line functions. Often called anonymous functions (because they don’t typically have a name), lambda functions are ideal for tasks that require a short and simple function.

To create a lambda function, we use the lambda keyword, followed by a list of arguments, a colon, and an expression.

Unlike regular Python functions defined with the def keyword, lambda functions only have a single expression and cannot contain statements. They also automatically return the result of the expression.

Typically, we use lambda functions with Python operators or in tandem with other functions like map(), filter(), and reduce() to operate on lists or other collections.

If you’re unfamiliar with these built-in functions, let’s summarize what they each do:

  • map() applies a function to all items in the provided iterable.
  • filter() filters items out of an iterable.
  • reduce() applies a rolling computation to sequential pairs of values in a list.

Let’s take a look at some general examples to see how lambda functions work. And to keep things realistic, we’ll also include examples that use map, filter, and reduce.

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Lambda Functions: General Examples
'''
# simple lambda example summing numbers
add = lambda x, y: x + y
print(add(2, 3))
# Result: 5

# Using lambda with map
numbers = [1, 2, 3, 4]
squared = list(map(lambda x: x**2, numbers))
# Result: [1, 4, 9, 16]

# Using lambda with filter
evens = list(filter(lambda x: x % 2 == 0, numbers))
# Result: [2, 4]

# Using lambda with reduce
from functools import reduce
product = reduce(lambda x, y: x * y, numbers)
# Result: 24

Now we know how to use lambda functions, let's look at a typical interview scenario:

Question: How can you use a lambda function to sort a list of strings by their length?

Answer Code:

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Lambda Functions: Interview Question
'''
words = ["apple", "kiwi", "banana", "grape"]
sorted_words = sorted(words, key=lambda x: len(x))
# Result: ['kiwi', 'apple', 'grape', 'banana']

5. Context Managers

Context managers in Python provide a neat and tidy way of acquiring and releasing resources when you need them.

By using the with statement, we can indicate when a resource should be acquired and when it should be released. The magic here is that the with block ensures that as soon as the operations are done, all cleanup operations are automatically taken care of.

For example, when dealing with files, you want to ensure that a file is closed after its operations are complete. In contrast, if we leave a file unclosed, it might cause resource leaks, which can result in unexpected behaviors or errors.

While files are the most common scenario for beginners to use context managers, they're also useful for managing other resources, such as network connections or database sessions. 

In general, we use context managers to avoid resource leaks, simplify error handling, and make code more readable. They’re a very Pythonic feature!

Let's look at the basic structure of a context manager by considering that when dealing with resources, you generally need to:

1. Acquire the resource.

2. Use the resource.

3. Release or clean up the resource.

Context managers automate and simplify steps 1 and 3 so that you can focus on step 2. Let’s take a look at a simple example for file operations. Here, you can see that by using the with statement, we can use the open function to open and assign a file to the file variable.

We can then process the file within the with block, which means that the file is automatically closed when we exit the with block. This happens even if exceptions are raised within the block.

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Context Managers: General Examples
'''
with open('sample.txt', 'r') as file:
  content = file.read()
# File is automatically closed after this block.

Let’s take a look at an example interview question involving context managers:

Question: How would you read a file and write its reversed content (line by line) into another file by using context managers to ensure all file resources are handled efficiently?

Answer Code:

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Context Managers: Interview Question
'''
with open('data.txt', 'r') as source_file:
  lines = source_file.readlines()

with open('reversed_data.txt', 'w') as target_file:
  for line in reversed(lines):
      target_file.write(line)

6. Deep vs. Shallow Copying

When dealing with data structures in Python, especially those with nested structures like lists within lists or dictionaries within dictionaries, it's super important to understand the difference between deep and shallow copying.

A shallow copy is when you construct a new collection object and then populate it with references to the child objects from the original. 

In essence, the objects are not copied, as the copy and the original reference the same objects. This means changes to items within the copy or the original will be reflected in both!

A deep copy recursively creates a new and separate copy of an entire object and its data. This means that when you change the copied object, the original remains unaffected.

In general, you need to be able to demonstrate how to use either approach while also showing that you understand the implications of each. This is also an essential Python concept for data-driven roles like data science and analytics.

Let’s look at creating shallow and deep copies with a simple example. 

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Deep vs Shallow Copying: General Example
'''
import copy

# Using a shallow copy
lst1 = [[1, 2, 3], [4, 5, 6]]
lst2 = copy.copy(lst1)
lst2[0][0] = 99

print(lst1)
# Result: [[99, 2, 3], [4, 5, 6]]

# Using a deep copy
lst1 = [[1, 2, 3], [4, 5, 6]]
lst3 = copy.deepcopy(lst1)
lst3[0][0] = 99

print(lst1)
# Result: [[1, 2, 3], [4, 5, 6]]

In this example, when we modify the shallow copy, the changes are also reflected in the original. Note that we could have used the list class copy method to create our shallow copy.

In contrast, by creating a deep copy, we were able to make changes without affecting the original list, as shown below.

Let’s now take a look at an interview question you might encounter that focuses on shallow vs deep copying:

Question: You're given a dictionary that represents the structure of a company. The dictionary contains departments as keys and a list of employees, where each is a dictionary. Each employee dictionary has the employee’s name and respective projects.

Create a new structure that renames a department without affecting the original company structure. Demonstrate how a shallow copy might introduce problems and how a deep copy can be used to avoid them.

Answer Code:

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Deep vs Shallow Copying: Interview Question
'''
import copy

company = {
  "IT": [
      {"name": "John", "projects": ["projectA", "projectB"]},
      {"name": "Jane", "projects": ["projectC"]}
  ],
  "HR": [
      {"name": "Doe", "projects": ["onboarding", "recruitment"]}
  ]
}

# Using a shallow copy
shallow_company = copy.copy(company)
shallow_company["Tech"] = shallow_company.pop("IT")

# Modifying an employee's projects in the new structure
shallow_company["Tech"][0]["projects"].append("projectD")

print(company["IT"][0]["projects"])
# Result: ['projectA', 'projectB', 'projectD']
# Problem: The original company structure is affected!

# Resetting our example
company = {
  "IT": [
      {"name": "John", "projects": ["projectA", "projectB"]},
      {"name": "Jane", "projects": ["projectC"]}
  ],
  "HR": [
      {"name": "Doe", "projects": ["onboarding", "recruitment"]}
  ]
}

# Using a deep copy
deep_company = copy.deepcopy(company)
deep_company["Tech"] = deep_company.pop("IT")

# Modifying an employee's projects in the new structure
deep_company["Tech"][0]["projects"].append("projectD")

print(company["IT"][0]["projects"])
# Result: ['projectA', 'projectB']
# Success: The original company structure remains unaffected!

7. The Collections Module

If you’ve been researching technical interviews, you’ve probably learned that they tend to include questions about data structures. This is absolutely true, and you should definitely be ready to create your own stack, queue, and linked list from scratch.

That said, it’s also really helpful to have a working knowledge of the Python Collections module, as this offers specialized container datatypes that are great alternatives to general-purpose containers like lists, dictionaries, and tuples. 

Let’s take a look at some of the most useful data structures in this module:

  • namedtuple: This allows you to define simple classes for storing data without any methods, which is useful when you want to store data with descriptive names for each position.
  • deque: This is a double-ended queue (see what they did with the name!) that supports fast appends and pops from both ends, making it ideal for queue and stack-based algorithms.
  • Counter: This is a subclass of the built-in dictionary that’s used for counting hashable objects.
  • OrderedDict: This is like a normal dictionary that also maintains the order of items based on their insertion order.
  • defaultdict: This is a lot like a dictionary, but it also provides a default value for keys that are not yet present. This concept will be familiar if you’ve used the dictionary setdefault method.

We should point out that if you’re asked to create a linked list or a queue, the interviewer is trying to test your understanding of those structures, so they’re unlikely to be impressed if you say that you’ll import a Python deque

But if you know that this structure exists and you know how to use it, you could impress your interviewer by creating your own data structure to solve the challenge while mentioning that as a pro developer, you’d naturally consider using this versus reinventing the wheel. 

This is a subtle difference, but can have a big impact on your interview performance! 

You should also be able to communicate that by using specialized containers, you can improve the clarity and performance of your code while also conveying your intent more clearly. 

It also helps to emphasize the performance gains from these specialized containers versus their general-purpose counterparts. For example, the Counter object allows you to easily tally occurrences without manually iterating and counting. 

Similarly, with the namedtuple, you can create readable and self-documenting code without the overhead of defining full-blown classes.

Let’s take a look at some general examples of how you can use the Collections module.

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
The Collections Module: General Examples
'''
from collections import namedtuple, deque, Counter

# namedtuple example
Employee = namedtuple('Employee', ['name', 'department'])
john = Employee(name="John", department="IT")
print(john.name)
# Result: John

# deque example
d = deque([1, 2, 3, 4])
d.appendleft(0)
d.pop()
print(d)
# Result: deque([0, 1, 2, 3])

# Counter example
colors = ['red', 'blue', 'red', 'green', 'blue', 'blue']
count = Counter(colors)
print(count)
# Result: Counter({'blue': 3, 'red': 2, 'green': 1})

Great, now, let’s take a look at an interview question that you might encounter that focuses on using the Collections module:

Question: Given a list of strings, how would you efficiently count the occurrence of each string using a container from the collections module?

Answer Code:

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
The Collections Module: General Examples
'''
from collections import Counter

words = ["apple", "banana", "apple", "grape", "banana", "grape", "apple"]
word_count = Counter(words)

print(word_count)
# Result: Counter({'apple': 3, 'banana': 2, 'grape': 2})

8. Difference Between __str__ & __repr__

When it comes to object-oriented programming, __str__ and __repr__ are two of the most useful dunder methods to add to your own classes. 

While these are both used to customize the string representation of objects, you need to know the difference between these two magic methods!

When it comes to __str__, this is intended for handling the pretty-print string representation of an object, typically for display to end-users. 

On the other hand, __repr__ is intended for providing the official or unambiguous string representation of an object, and it’s primarily for development and debugging. 

This means that the __repr__ string needs to be as explicit as possible, and ideally, it should be a valid Python expression that could be used to recreate the object.

The TL-DR here is that you should aim to design __repr__ to return executable Python code to recreate the object, and __str__ should prioritize readability for the end user.

It’s also good to show that you would at least define __repr__  to ensure classes have meaningful print-outs and that __repr__ can also be used for print() function calls if you don’t include __str__

Let's look at how to implement these methods and how they work with a simple example, as shown below.

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
__str__ & __repr: General Examples
'''
class Person:
  def __init__(self, name, age):
      self.name = name
      self.age = age

  def __str__(self):
      return f"{self.name}, {self.age} years old"

  def __repr__(self):
      return f"Person('{self.name}', {self.age})"

alice = Person("Alice", 30)

print(str(alice))  # Result: Alice, 30 years old
print(repr(alice)) # Result: Person('Alice', 30)
repr_string = repr(alice)
new_person = eval(repr_string)
print(alice == new_person) # This will return False

In this example, we can see that the __str__ method returns a formatted string literal (f-string) that contains a friendly print statement to summarize the Person object.

In contrast, the __repr__ method returns a string object that can be used to instantiate a new instance of a Person object. This is done by calling the eval function on the __repr__ string, which calls the Person class constructor.

By doing this, we can see that the new_person object is not equal to the original Person object, indicating that we’ve created a new instance of a Person object with the same parameters.

Let’s now look at a potential interview question related to these methods:

Question: Suppose you're given a class definition without __str__ and __repr__. How would you add them to the class?

Answer Code:

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
__str__ & __repr: Interview Question
'''
class Point:
  def __init__(self, x, y):
      self.x = x
      self.y = y

  def __str__(self):
      return f"({self.x}, {self.y})"

  def __repr__(self):
      return f"Point({self.x}, {self.y})"

p = Point(3, 4)
print(str(p)) # Result: (3, 4)
print(repr(p)) # Result: Point(3, 4)
new_p = eval(repr(p))
print(p == new_p) # This will return False

9. Metaclasses & Class Factories

Let’s move on to more difficult Python techniques, starting with metaclasses and class factories, which are both advanced object-oriented concepts that deal with the dynamic creation and modification of classes.

Of course, we all know that Python is well-known for being dynamic, but metaclasses and class factories take this a step further!

But what are these Python concepts?

  • Metaclasses: Often known as classes of classes, these control the creation and behavior of classes, similar to how classes control the creation and behavior of object instances. The default metaclass in Python is type.
  • Class Factories: These are functions or methods that return classes, and they can be useful in scenarios where classes need to be generated dynamically based on certain parameters or conditions.

To truly grasp these ideas, it's always a good idea to look at some practical examples.

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Metaclasses & Class Factories: General Examples
'''
# Metaclass example: Ensuring all class attributes are in uppercase
class UppercaseAttributesMeta(type):
  def __new__(cls, name, bases, dct):
      uppercase_attributes = {
          key.upper(): val for key, val in dct.items()
      }
      return super().__new__(cls, name, bases, uppercase_attributes)

class MyClass(metaclass=UppercaseAttributesMeta):
  lower_case = "I should be uppercase."

print(hasattr(MyClass, "lower_case"))  # Result: False
print(hasattr(MyClass, "LOWER_CASE"))  # Result: True

# Class Factory example: Creating classes based on a color parameter
def ColorClassFactory(color):
  class ColorClass:
      def __str__(self):
          return f"I am a {color} class!"
  return ColorClass

RedClass = ColorClassFactory("Red")
red_instance = RedClass()
print(str(red_instance))  # Result: I am a Red class!

In this example, the UppercaseAttributesMeta metaclass alters a class definition by converting all attribute names to uppercase during the creation of a class. 

This is done by using the __new__ dunder method, which, in the context of metaclasses, is invoked during class creation. 

We’ve then used a dictionary comprehension in the __new__ method to transform the attributes from the original class dictionary to uppercase. 

The effect is demonstrated by MyClass, where we can see that the attribute names, even when defined in lowercase in the class body, are transformed to uppercase.

Moving on to the class factory, the function ColorClassFactory is a class factory that internally defines and returns a class named ColorClass

We’ve then defined behavior for this class via the __str__ dunder method, and this uses the color parameter that’s passed to the class factory function.

We can now allow users to dynamically generate classes with specific behaviors without manually defining each.

This is shown by creating RedClass using the factory, which, when instantiated and converted to a string, outputs a message to say it is Red.

Now, let's consider a typical interview question related to metaclasses and class factories:

Question: How can you implement a metaclass that ensures the classes it creates are singletons?

Answer Code:

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Metaclasses & Class Factories: Interview Questions
'''
# Singleton Metaclass
class SingletonMeta(type):
  _instances = {}

  def __call__(cls, *args, **kwargs):
      if cls not in cls._instances:
          instance = super().__call__(*args, **kwargs)
          cls._instances[cls] = instance
      return cls._instances[cls]


class SingletonClass(metaclass=SingletonMeta):
  pass

instance1 = SingletonClass()
instance2 = SingletonClass()

print(instance1 == instance2)  # Result: True

10. Asynchronous Programming & asyncio

Asynchronous programming allows multiple tasks to be started and processed simultaneously, improving the efficiency of I/O tasks, and enhancing the overall performance of your programs. 

This is hugely helpful when dealing with things like network requests, reading/writing files, and other operations that take time to complete and would otherwise block program execution.

This is where Python's asyncio library comes in, as we can use it to write single-threaded, concurrent, and non-blocking code. This revolves around the event loop, async/await syntax, and coroutine functions.

But how does this all work? Well, by using the async keyword before a function, we can define a coroutine, which can be paused and resumed at many points asynchronously.

We can then use the await keyword to call a coroutine function and wait for its completion without blocking the execution of the rest of the program. 

This might be a little confusing if you’re new to the concept, so let’s take a look at a simple example to make things clearer!

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Asynchronous Programming & asyncio: General Examples
'''
import asyncio

async def print_numbers():
  for i in range(5):
      print(i)
      await asyncio.sleep(1)

async def print_letters():
  for letter in 'abcde':
      print(letter)
      await asyncio.sleep(1)

async def main():
  await asyncio.gather(print_numbers(), print_letters())

asyncio.run(main())
# Results: 0, a, 1, b, 2, c, 3, d, 4, e

In this example, we’ve created two simple async functions or coroutines. One of these prints numbers in a range, and the other prints characters from a string.

Within each function, we use the asyncio sleep function to impose a 1-second pause after printing a number or letter. Notice that whenever we want to call an async function, whether that’s a built-in function or our own, we use the await keyword.

We’ve then created an async main function. Within this, we use the await keyword followed by the asyncio gather function to call our async coroutines. We finally call the main function by using the asyncio run function.

But what do you think happens next? 

  • The first number is printed, followed by a 1-second pause within the print_numbers for loop. But being asynchronous, this frees the program to run the print_letters function.
  • The print_letters function then prints the first letter before imposing a 1-second pause within its for loop. 
  • At this point, the print_numbers function will be the first to finish its 1-second pause, so it will resume to print another number, before pausing again. 
  • The print_letters function will then be free to execute again after completing its previous 1-second pause, allowing it to print another letter before pausing again for 1 second.

This whole process will repeat for each number and letter, and it finishes when they’ve all been printed.

The magic here is that the program does not have to wait for a 1-second time delay to elapse before calling the next function. This is why we say the code is non-blocking.

The net result is that we see an alternation of printed numbers and letters.

And if you watch the screen output, it will feel like you are waiting for 1-second intervals before seeing a number and letter printed at the same time.

Interestingly, this program takes a little more than 5 seconds to complete, as while each function imposes a 1-second delay, the asynchronous nature means they run concurrently.

Note that the reason it takes more than 5 seconds is down to the negligible time it takes to switch between tasks and print the numbers and letters.

In contrast, if these were standard functions and we ran them sequentially, it would take around 10 seconds to complete, and we’d end up printing all numbers followed by all letters.

Now, let's take a look at a typical interview question related to asynchronous programming:

Question: Write an asynchronous function that waits for a given number of seconds and then prints a given message, repeating this process a given number of times.

Answer Code:

'''
Hackr.io: 10 Python Concepts I Wish I Knew Earlier:
Asynchronous Programming & asyncio: Interview Questions
'''
import asyncio

async def repeat_after(interval, message, times):
  for _ in range(times):
      await asyncio.sleep(interval)
      print(message)


asyncio.run(repeat_after(1, "Hello, World!", 3))
# Result: Hello, World!, Hello, World!, Hello, World!

Python Concepts I Wish I Knew Earlier: Wrapping Up

So there you have it, the 10 Python concepts I wish I knew earlier for acing interviews, including explanations, code examples for the Python concepts, and potential interview questions that use these essential Python concepts.

Whether you’re new to Python and looking to land your first job, or fresh out of college and looking to get on the first rung of the career ladder, learning these 10 Python concepts can help you stand out from the crowd!

Happy learning, and best of luck at your next Python interview!

Enjoyed learning these Python concepts and are ready to dive deeper into Python? Check out:

Our Python Masterclass - Python with Dr. Johns

Frequently Asked Questions

1. What Are The Hardest Topics In Python?

This really depends on your current skill level, educational background, and previous programming experience. That said, some of the hardest topics for Python students include recursion, memory management, decorators, metaclasses, concurrency, generators, type annotations, asynchronous programming, and functional programming. 

2. How Long Does It Take To Master Python?

The time it takes to master Python really depends on your individual goals and prior experience. Beginners can expect to grasp the basics in a matter of weeks or months while reaching an intermediate level could take up to a year. Achieving advanced proficiency in Python, especially in specialized areas like data science, machine learning, or AI, can take several years. 

3. Is Python Worth Learning In 2024?

Absolutely! Python continues to be a versatile, powerful, and popular programming language with diverse applications, from web development and data science to artificial intelligence and automation. Plus, its strong community, abundant libraries, and ease of learning make it a top choice for beginners and seasoned developers.

People are also reading:

By Robert Johns

Technical Editor for Hackr.io | 15+ Years in Python, Java, SQL, C++, C#, JavaScript, Ruby, PHP, .NET, MATLAB, HTML & CSS, and more... 10+ Years in Networking, Cloud, APIs, Linux | 5+ Years in Data Science | 2x PhDs in Structural & Blast Engineering

View all post by the author

Subscribe to our Newsletter for Articles, News, & Jobs.

I accept the Terms and Conditions.
Thanks for subscribing! Look out for our welcome email to verify your email and get our free newsletters.

Disclosure: Hackr.io is supported by its audience. When you purchase through links on our site, we may earn an affiliate commission.

In this article

Learn More

Please login to leave comments