SOLVED (1–10): Think You Know Python? These 20 Riddles Will Make You Think Again!

Moraneus
10 min readJul 26, 2024

--

Welcome back, Python enthusiasts and curious coders! Today, I’m going to unravel the mysteries behind my fascinating Python riddles 1–10, published in my previous article.

Each of these riddles showcases a unique aspect of Python, and understanding them will significantly boost your Python proficiency. If you are curious about your Python skills, review first the 20 Pyhton Riddles Article before reading this article.

Also, don’t forget to share your experience in the comments section below. How many riddles did you solve correctly? Which ones stumped you? Did you learn something new? Your feedback and insights are valuable to me and other readers.

Let’s dive in!

Riddle 1: List mutability

x = [1, 2, 3]
y = x
y.append(4)
print(x)
Output: [1, 2, 3, 4]

At first glance, you might expect x to remain [1, 2, 3]. After all, we only modified y, right? Wrong! This riddle unveils a fundamental characteristic of Python: list mutability and reference sharing.

In Python, when you assign a list to a variable, you’re not creating a new list. Instead, you’re creating a new reference to the existing list. So when we do y = x, both x and y are pointing to the same list object in memory.

Here’s a visual representation:

x ----→ [1, 2, 3]
y ----↗

When we append 4 to y, we're modifying the list that both x and y refer to:

x ----→ [1, 2, 3, 4]
y ----↗

This behavior is crucial to understand for effective Python programming. It can lead to unexpected results if you’re not careful, but it also allows for efficient memory usage.

Best Practice: If you need a separate copy of a list, use the copy() method or slice notation [:]:

y = x.copy()  # or y = x[:]

Riddle 2: Default mutable arguments

def func(a, b=[]):
b.append(a)
return b

print(func(1))
print(func(2))
print(func(3))
Output:
[1]
[1, 2]
[1, 2, 3]

This riddle exposes a common pitfall in Python: mutable default arguments. The trap here is that the default argument b=[] is evaluated only once, when the function is defined, not each time the function is called.

This means that the same list object is used as the default value for b across all calls to func where b is not explicitly provided. Each call to func modifies this same list, leading to the accumulating effect we see in the output.

Here’s what’s happening step by step:

  1. First call func(1):
  • a = 1, b is the default empty list []
  • 1 is appended to b
  • Returns [1]

2. Second call func(2):

  • a = 2, b is the same list from the previous call, now containing [1]
  • 2 is appended to b
  • Returns [1, 2]

3. Third call func(3):

  • a = 3, b is the same list, now containing [1, 2]
  • 3 is appended to b
  • Returns [1, 2, 3]

Best Practice: To avoid this behavior, use None as the default value and create a new list inside the function:

def func(a, b=None):
if b is None:
b = []
b.append(a)
return b

This version creates a new list for each call when b is not provided, avoiding the accumulation effect.

Riddle 3: Multiple assignment

a = 1
b = 2
a, b = b, a
print(a, b)
Output: 2 1

This riddle showcases one of Python’s most elegant features: multiple assignment. It allows us to swap variables without needing a temporary variable, as you might do in other languages.

Here’s how it works:

  1. The right side of the assignment b, a is evaluated first, creating a tuple (2, 1).
  2. This tuple is then unpacked and assigned to a and b respectively.

It’s worth noting that this happens simultaneously, so there’s no risk of one variable being overwritten before the other is assigned.

This technique isn’t limited to just two variables. You can use it with any number of variables:

a, b, c = c, a, b  # rotates the values of a, b, and c

Fun Fact: This multiple assignment feature makes it easy to write concise code for algorithms like the Fibonacci sequence:

a, b = 0, 1
for _ in range(10):
print(a, end=' ')
a, b = b, a + b
# Output: 0 1 1 2 3 5 8 13 21 34

Riddle 4: Boolean arithmetic

print(True + True + True)
Output: 3

This riddle might seem counterintuitive at first, but it demonstrates an interesting aspect of Python: booleans are a subclass of integers.

In Python:

  • True has a numeric value of 1
  • False has a numeric value of 0

When used in arithmetic operations, True is treated as 1 and False as 0. So, this expression is equivalent to 1 + 1 + 1, which equals 3.

This behavior can be both useful and dangerous. It allows for concise code in some cases, but it can also lead to subtle bugs if you’re not careful.

For example, you can do things like:

count = sum([True, False, True, True])  # count will be 3

But be careful with expressions like:

if True == 1:
print("This will actually print!")

While this works, it’s generally considered bad practice to rely on the numeric values of booleans. It’s clearer and safer to use proper boolean operations.

Riddle 5: String indexing and slicing

print("hello"[-1] + "world"[1])
Output: oo

This riddle demonstrates the power and flexibility of Python’s string indexing and slicing.

Let’s break it down:

  1. "hello"[-1]: In Python, negative indices count from the end of the sequence. -1 refers to the last character, so this gives us "o".
  2. "world"[1]: Regular (positive) indices start from 0, so index 1 gives us the second character, "o".

Concatenating these results gives us “oo”.

String indexing in Python is incredibly versatile:

  • Positive indices count from the start (0-based)
  • Negative indices count from the end (-1 for the last character)
  • You can use slicing with a start:end:step syntax, e.g., "hello"[1:4] gives "ell"
  • Omitting slice indices uses defaults: start=0, end=len(string), step=1

Here are some more examples:

s = "Python"
print(s[0]) # "P"
print(s[-2]) # "o"
print(s[1:4]) # "yth"
print(s[::-1]) # "nohtyP" (reverses the string)

Best Practice: While clever use of string slicing can lead to concise code, always prioritize readability. If a slicing operation is complex, consider breaking it down or adding a comment to explain what it does.

Riddle 6: F-strings and formatting

x = 10
y = 50
print(f"{x}% of {y} is {x/100*y}")

Output: 10% of 50 is 5.0

This riddle showcases one of Python 3’s most loved features: f-strings (formatted string literals).

F-strings provide a concise and readable way to embed expressions inside string literals. They’re prefixed with ‘f’ and use curly braces {} to enclose expressions.

Here’s what’s happening in our riddle:

  • {x} is replaced with the value of x (10)
  • {y} is replaced with the value of y (50)
  • {x/100*y} is evaluated as an expression (10/100 * 50 = 5.0)

F-strings are not just for simple variable substitution. You can put any valid Python expression inside the curly braces, including function calls, arithmetic operations, or even nested f-strings!

Some more examples:

name = "Alice"
age = 30
print(f"{name.upper()} is {age * 12} months old.")
# Output: ALICE is 360 months old.

import math
radius = 5
print(f"A circle with radius {radius} has an area of {math.pi * radius**2:.2f}")
# Output: A circle with radius 5 has an area of 78.54

Best Practice: F-strings are great for readability, but be cautious about putting too complex expressions inside them. If an expression is complicated, it’s often clearer to calculate it separately and then include the result in the f-string.

Riddle 7: Operator precedence

print(2 * 3 ** 3 * 2)
Output: 108

This riddle tests your understanding of operator precedence in Python. If you’re not careful, you might think this evaluates to 432 (if calculated left to right).

Here’s the correct order of operations:

  1. Exponentiation (**) has the highest precedence, so 3 ** 3 is evaluated first, giving 27.
  2. Then the multiplications are performed left to right: 2 * 27 * 2.

So the expression is equivalent to:

print(2 * (3 ** 3) * 2)  # 2 * 27 * 2 = 54

Python follows the PEMDAS rule (Parentheses, Exponents, Multiplication/Division, Addition/Subtraction), with a few additional rules:

  • Exponentiation (**) is right-associative, meaning a**b**c is interpreted as a**(b**c), not (a**b)**c.
  • Multiplication and division have the same precedence and are left-associative.
  • Same for addition and subtraction.

Here’s a more complex example:

print(2 + 3 * 4 ** 2 - 6 / 2)  # What's the result?

It evaluates as:

  1. 4 ** 2 = 16
  2. 3 * 16 = 48
  3. 6 / 2 = 3
  4. 2 + 48 - 3 = 47

Best Practice: When in doubt, use parentheses to make your intentions clear. It’s better to be explicit than to rely on operator precedence rules that not all readers might remember.

Riddle 8: Function returns and string concatenation

def greet(name):
return f"Hello, {name}!"

print(greet("Alice") + " " + greet("Bob"))
Output: Hello, Alice! Hello, Bob!

This riddle combines function calls with string concatenation. Let’s break it down:

  1. greet("Alice") returns the string "Hello, Alice!"
  2. greet("Bob") returns the string "Hello, Bob!"
  3. These two strings are concatenated with a space " " in between

This demonstrates a few important Python concepts:

  • Functions can return values, which can be immediately used in expressions.
  • String concatenation using the + operator.
  • F-strings can be used inside functions to create formatted strings.

You could achieve the same result in different ways:

# Using an f-string
print(f"{greet('Alice')} {greet('Bob')}")

# Using str.format()
print("{} {}".format(greet("Alice"), greet("Bob")))

# Using the join method
print(" ".join([greet("Alice"), greet("Bob")]))

Best Practice: When concatenating many strings, consider using str.join() or f-strings for better performance and readability, especially if you're building the string iteratively.

Riddle 9: Range function

print(list(range(5, 0, -1)))
Output: [5, 4, 3, 2, 1]

This riddle showcases the versatility of Python’s range() function and how it can be used to generate sequences in reverse order.

The range() function can take up to three arguments:

  • start: the first number in the sequence (inclusive)
  • stop: the last number in the sequence (exclusive)
  • step: the difference between each number in the sequence

In this case:

  • start is 5
  • stop is 0 (exclusive, so the sequence will end at 1)
  • step is -1, meaning we're counting down

The list() function is then used to convert the range object into a list, making it easier to print and visualize.

Here are some more examples of range():

print(list(range(5)))          # [0, 1, 2, 3, 4]
print(list(range(2, 8))) # [2, 3, 4, 5, 6, 7]
print(list(range(0, 10, 2))) # [0, 2, 4, 6, 8]
print(list(range(10, 0, -2))) # [10, 8, 6, 4, 2]

Best Practice: range() is memory-efficient because it doesn't store all the numbers in memory at once. If you just need to iterate over the numbers, use range() directly in a for loop without converting to a list:

for i in range(5, 0, -1):
print(i, end=' ')
# Output: 5 4 3 2 1

Riddle 10: String formatting with .format()

print("{2}, {1}, {0}".format('a', 'b', 'c'))
Output: c, b, a

This riddle demonstrates the flexibility of Python’s string formatting using the str.format() method.

In this method:

  • Curly braces {} in the string are placeholders for arguments.
  • The numbers inside the braces refer to the index of the arguments passed to format().
  • Arguments are 0-indexed, so {0} refers to the first argument, {1} to the second, and so on.

This allows us to reorder the arguments in the output string however we like. In this case, we’re reversing the order of the arguments.

The str.format() method is incredibly versatile. Here are some more examples:

# Reusing arguments
print("{0} {1} {0}".format("hello", "world")) # Output: hello world hello

# Named arguments
print("{name} is {age} years old".format(name="Alice", age=30)) # Output: Alice is 30 years old

# Accessing object attributes
class Point:
def __init__(self, x, y):
self.x, self.y = x, y

p = Point(4, 5)
print("The point is at ({0.x}, {0.y})".format(p)) # Output: The point is at (4, 5)

# Specifying format
print("Pi is approximately {:.2f}".format(3.14159)) # Output: Pi is approximately 3.14

While str.format() is powerful and still widely used, Python 3.6+ introduced f-strings, which offer similar functionality with a more concise syntax:

name = "Alice"
age = 30
print(f"{name} is {age} years old") # Output: Alice is 30 years old

Best Practice: For simple formatting, f-strings are often preferred due to their readability. For more complex formatting or when you need to reuse a format string, str.format() is still very useful. Always choose the method that makes your code most readable and maintainable.

Thats All For Now

We’ve journeyed through the first 10 Python riddles displayed in my previous article, unraveling mysteries of list mutability, default arguments, multiple assignment, boolean arithmetic, string manipulation, f-strings, operator precedence, function returns, the range function, and string formatting. Each riddle has peeled back a layer of Python’s inner workings, revealing the elegant and sometimes surprising nature of the language.

But our adventure doesn’t end here! In the next part of this series, I’ll tackle riddles 11 through 20. These will introduce us to other fascinating Python concepts.

Stay tuned for more mind-bending Python riddles and in-depth explanations. Whether you’re a beginner looking to deepen your understanding or an experienced developer aiming to sharpen your skills, there’s always something new to discover in the world of Python.

Remember, the key to mastering these concepts is practice. Try modifying these riddles, experiment with the code, and see how changes affect the output. Python’s interactive environment is your playground — don’t be afraid to explore!

Join me in the next part as we continue our Python riddle adventure. Until then, happy coding!

--

--