Python has several pieces of syntax that are syntactic sugar. This sugar is syntax that isn’t strictly necessary but gives Python some of its flavor as a readable, beginner-friendly, and powerful language. In this tutorial, you’ll explore some of Python’s most used pieces of syntactic sugar.
In practice, you already use most of these pieces of syntax, as they include many well-known Pythonic constructs. As you read on, you’ll see how Python works under the hood and learn how to use the language efficiently and securely.
In this tutorial, you’ll learn:
- What syntactic sugar is
- How syntactic sugar applies to operators
- How assignment expressions are syntactic sugar
- How
for
loops and comprehensions are syntactic sugar - How other Python constructs are also syntactic sugar
To get the most out of this tutorial, you should be familiar with the basics of Python, including operators, expressions, loops, decorators, classes, context managers, and more.
Get Your Code: Click here to download the free sample code that shows you how to use syntactic sugar in Python.
Take the Quiz: Test your knowledge with our interactive “Syntactic Sugar: Why Python Is Sweet and Pythonic” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Syntactic Sugar: Why Python Is Sweet and PythonicYou can take this quiz to test your understanding of Python's most common pieces of syntactic sugar and how they make your code more Pythonic and readable.
Syntactic Sugar
In programming, syntactic sugar refers to pieces of syntax that simplify the code and make it more readable or concise. Syntactic sugar lets you express things in a clearer and more readable way.
It makes the language sweeter for human use: things can be expressed more clearly, more concisely, or in an alternative style that some may prefer. (Source)
However, syntactic sugar is something that you may not need in practice because you can get the same result using a different, and often more involved, construct.
Note: This tutorial is slightly inspired by Brett Cannon’s series of posts about unraveling syntactic sugar in Python. In that series, Brett goes deep into each piece of syntactic sugar. You can check out the series if you’d like a detailed discussion of the syntax constructs covered in this tutorial and others.
Python has many pieces of syntactic sugar that you’ll regularly use in your code. These syntax constructs make Python more readable, quicker to write, and user-friendly. Understanding these syntactic sugar pieces and their significance will help you better understand the inner workings of Python.
In rare situations, you’ll find that desugared versions of a given piece of syntactic sugar can better fulfill your needs. So, knowing about the alternative code to a given sugar can be a good skill to have.
Operators in Python
As with most programming languages, Python makes extensive use of operators. You’ll find several categories of operators, including arithmetic, assignment, augmented assignment, comparison, Boolean, and membership operators. All these operators are part of Python’s syntactic sugar constructs because they let you write expressions in a quick and readable way.
Note: To dive deeper into Python operators, check out the Operators and Expressions in Python tutorial.
For example, arithmetic operators allow you to create math expressions that are quick to write and read because they look pretty similar to what you learned in math class:
>>> 5 + 7
12
>>> 10 - 4
6
>>> 2 * 4
8
>>> 20 / 2
10
In the first example, you use the plus operator (+
) to add two numbers. In the second example, you use the subtraction operator (-
) to subtract two numbers. The final two examples perform multiplication and division.
Python supports its arithmetic operators through special methods. Here’s a quick summary:
Operator | Operation | Method |
---|---|---|
+ |
Addition | .__add__() |
- |
Subtraction | .__sub__() |
* |
Multiplication | .__mul__() |
/ |
Division | .__truediv__() |
// |
Integer division | .__floordiv__() |
** |
Exponentiation | .__pow__() |
What does it mean to say Python supports its operators through special methods? It means that every time you use an operator, Python calls the corresponding special method under the hood.
Note: To learn more about special methods, also known as magic or dunder methods, check out Python’s Magic Methods: Leverage Their Power in Your Classes.
To illustrate, here’s how you can express the arithmetic operations you wrote earlier using the appropriate special methods:
>>> 5 + 7
12
>>> (5).__add__(7)
12
>>> 10 - 4
6
>>> (10).__sub__(4)
6
>>> 2 * 4
8
>>> (2).__mul__(4)
8
>>> 20 / 2
10
>>> (20).__truediv__(2)
10
In these examples, you first have the usual way to write an arithmetic expression using the operators, and then you have the equivalent construct using the corresponding special method.
As you can see, using the special method construct makes your code harder to read and understand. So, the operators make your life easier and your code more readable. They’re syntactic sugar.
If you consider augmented assignment operators, then you realize that they’re an even better example of a syntactic sugar construct. Here’s an example of an augmented addition:
>>> count = 0
>>> count += 1
>>> count
1
In this example, the +=
symbol is the augmented addition operator, which allows you to sum a value on top of an existing variable. This expression is a shortcut for the following expression:
>>> count = 0
>>> count = count + 1
>>> count
1
As you can conclude, both expressions are equivalent. However, the expression using the +=
operator is quicker to write.
Note: For a list like numbers = [1, 2, 3]
, something like numbers = numbers + [4]
will create a new list
object. On the other hand, something like numbers += [4]
would mutate the numbers
in place. So, even though you can replace the augmented operators with equivalent expressions and get the same apparent result, you need to know that with mutable objects, the internal behavior is different.
When it comes to comparison operators, you’ll find that you can also replace them with special methods:
Operator | Operation | Method |
---|---|---|
< |
Less than | .__lt__() |
<= |
Less than or equal | .__le__() |
> |
Greater than | .__gt__() |
>= |
Greater than or equal | .__ge__() |
== |
Equality | .__eq__() |
!= |
Inequality | .__ne__() |
With these methods, you can create constructs that work like usual comparison expressions. Consider the following examples of expressions and their equivalent method calls:
>>> 2 < 1
False
>>> (2).__lt__(1)
False
>>> 3 <= 7
True
>>> (3).__le__(7)
True
>>> 5 > 3
True
>>> (5).__gt__(3)
True
>>> 3 >= 7
False
>>> (3).__ge__(7)
False
>>> 1 == 1
True
>>> (1).__eq__(1)
True
>>> 1 != 1
False
>>> (1).__ne__(1)
False
In these examples, you confirm that all the comparison operators are syntactic sugar because you can replace them with method calls.
You also have the in
and not in
operators to run membership tests. A membership test allows you to check whether a value is in a given collection of values:
>>> 5 in [1, 2, 3, 4, 5]
True
>>> 5 not in [1, 2, 3, 4, 5]
False
>>> 100 in [1, 2, 3, 4, 5]
False
>>> 100 not in [1, 2, 3, 4, 5]
True
Because 5
is in the list of values, in
returns True
and not in
returns False
. Similarly, because 100
isn’t in the list, in
returns False
and not in
returns True
.
Note: To learn more about membership tests, check out the Python’s “in” and “not in” Operators: Check for Membership tutorial.
In practice, you can replace these operators with calls to the .__contains__()
method:
>>> [1, 2, 3, 4, 5].__contains__(5)
True
>>> not [1, 2, 3, 4, 5].__contains__(5)
False
>>> [1, 2, 3, 4, 5].__contains__(100)
False
>>> not [1, 2, 3, 4, 5].__contains__(100)
True
Alternatively, you can implement the algorithm in a function that iterates over the values in the target iterable:
>>> def is_member(value, iterable):
... for current_value in iterable:
... if current_value == value:
... return True
... return False
...
>>> is_member(5, [1, 2, 3, 4, 5])
True
>>> not is_member(5, [1, 2, 3, 4, 5])
False
>>> is_member(100, [1, 2, 3, 4, 5])
False
>>> not is_member(100, [1, 2, 3, 4, 5])
True
In this example, the is_member()
function takes a target value and an iterable as arguments. Then, it checks whether the value is in the iterable using a loop. The result of calling the function is equivalent to using the membership operators.
Chained Conditions
Sometimes, you have two comparisons joined with an and
operator. For example, say that you want to know whether a number is in a given interval. In this situation, you can do something like the following:
>>> number = 5
>>> if number >= 1 and number <= 10:
... print("Inside interval")
... else:
... print("Outside interval")
...
Inside interval
With the and
expression shown in this example, you can check if a given number is in the interval from 1
to 10
, both included.
Python has a shortcut to express the same condition. You can use chained operators as shown below:
>>> if 1 <= number <= 10:
... print("Inside interval")
... else:
... print("Outside interval")
...
Now, your condition doesn’t explicitly include the and
operator. However, it produces the same result. So, both conditions are equivalent.
Note: You can chain operators in different ways. For a deeper dive into this topic, check out the Chaining Comparison Operators section in the Python Booleans: Use Truth Values in Your Code tutorial.
Chaining operators, as you did in this example, is another syntactic sugar piece in Python.
Ternary Operator
Python has a syntax construct known as the ternary operator or conditional expressions. These expressions are inspired by the ternary operator that looks like a ? b : c
and is used in other programming languages, such as C.
This construct evaluates to b
if the value of a
is true, and otherwise evaluates to c
. Because of this, sometimes the equivalent Python syntax is also known as the ternary operator, and it looks as shown below:
variable = expression_1 if condition else expression_2
This expression returns expression_1
if the condition is true and expression_2
otherwise. In practice, this syntax is equivalent to a conditional like the following:
if condition:
variable = expression_1
else:
variable = expression_2
Because you can replace the ternary operator with an equivalent if
… else
statement, you can say that this operator is another piece of syntactic sugar in Python.
Assignment Expressions
Traditional assignments built with the =
operator don’t return a value, so they’re statements but not expressions. In contrast, assignment expressions are assignments that return a value. You can build them with the walrus operator (:=
).
Assignment expressions allow you to assign the result of an expression used, say, in a conditional or a while
loop to a name in one step. For example, consider the following loop that takes input from the keyboard until you type the word "stop"
:
>>> while (line := input("Type some text: ")) != "stop":
... print(line)
...
Type some text: Python
Python
Type some text: Walrus
Walrus
Type some text: stop
In this example, you get the user’s input in the line
variable using an assignment expression. At the same time, the expression returns the user’s input so that it can be compared to the sentinel value, "stop"
.
Note: To dive deeper into assignment expressions, check out The Walrus Operator: Python’s Assignment Expressions.
In practice, you rewrite this loop without using the walrus operator by lifting the variable assignment:
user_input.py
line = input("Type some text: ")
while line != "stop":
print(line)
line = input("Type some text: ")
This code snippet works the same as the code you wrote above using the walrus operator. However, the operator isn’t in the equation anymore. So, you can conclude that this operator is another piece of syntactic sugar in Python.
This example has an additional drawback: It unnecessarily repeats the call to input()
, which doesn’t happen with the syntactic-sugared version of the code. You can skip the repetition using a while
loop like the following:
user_input.py
while True:
line = input("Type some text: ")
if line == "stop":
break
print(line)
This loop avoids the repetition. However, the entire code is now a bit harder to understand because the condition is buried within the loop.
Unpacking in Python
Iterable unpacking is one of those lovely features of Python. Unpacking an iterable means assigning its values to a series of variables one by one. In the following sections, you’ll learn how iterable unpacking is another piece of syntactic sugar.
Iterable Unpacking
Iterable unpacking is a powerful feature that can be used in various situations. It can help you write more readable and concise code.
You can use unpacking to distribute the values in an iterable into a series of variables:
>>> one, two, three, four = [1, 2, 3, 4]
>>> one
1
>>> two
2
>>> three
3
>>> four
4
In this example, you have a tuple of variables on the left and a list of values on the right. Once Python runs this assignment, the values are unpacked into the corresponding variable by position.
You can replace this code construct with the following series of assignments:
>>> numbers = [1, 2, 3, 4]
>>> one = numbers[0]
>>> one
1
>>> two = numbers[1]
>>> two
2
>>> three = numbers[2]
>>> three
3
>>> four = numbers[3]
>>> four
4
In this case, you manually assign the values to each variable using the corresponding indexing operation. This code is less readable than the previous version but produces the same result.
An excellent use case for unpacking is when you need to swap values between variables. In languages that don’t have an unpacking feature, you’ll have to use a temporary variable:
>>> a = 200
>>> b = 400
>>> temp = a
>>> a = b
>>> b = temp
>>> a
400
>>> b
200
In this example, you use the temp
variable to hold the value of a
so that you can swap the values between a
and b
. Using the unpacking syntactic sugar, you can do something like the following:
>>> a = 200
>>> b = 400
>>> a, b = b, a
>>> a
400
>>> b
200
In this example, the highlighted line does the magic, allowing you to swap values in a clean and readable way.
*args
and **kwargs
In Python, you can define functions that take an undefined number of positional or keyword arguments using the *args
and **kwargs
syntax in the function’s definition. The *args
argument packs a series of positional arguments into a tuple.
Note: You’ll typically see the args
and kwargs
names used as generic names in functions that use the *args
and **kwargs
syntax. However, the names are just a convention. In practice, you can use any name. The *
and **
are the required elements in this syntax. So, you can also use names like *numbers
and **options
or whatever names you find appropriate for your use case.
Consider the following toy example:
>>> def show_args(*args):
... print(args)
...
>>> show_args(1, 2, 3, 4)
(1, 2, 3, 4)
This function uses the *args
syntax to tell Python that it can take an undetermined number of positional arguments. When you call the function with positional arguments, they’re packed into a tuple.
Note: To learn more about *args
and **kwargs
, check out the Python args
and kwargs
: Demystified tutorial.
On the other hand, the **kwargs
argument packs keyword arguments into a dictionary:
>>> def show_kwargs(**kwargs):
... print(kwargs)
...
>>> show_kwargs(one=1, two=2, three=3, four=4)
{'one': 1, 'two': 2, 'three': 3, 'four': 4}
In this example, you use the **kwargs
syntax to tell Python that this function will take an undetermined number of keyword arguments.
Note: In Python code, it’s common to find *args
and **kwargs
used together:
>>> def show_arguments(*args, **kwargs):
... print(f"{args = }")
... print(f"{kwargs = }")
...
>>> show_arguments(1, 2, 3, 4, one=1, two=2, three=3, four=4)
args = (1, 2, 3, 4)
kwargs = {'one': 1, 'two': 2, 'three': 3, 'four': 4}
In this example, your show_arguments()
accepts both *args
and **kwargs
. You can call the function with a series of positional arguments followed by keyword arguments.
You can replace *args
with a list or tuple and **kwargs
with a dictionary:
>>> def show_values(values):
... print(values)
...
>>> show_values([1, 2, 3, 4])
[1, 2, 3, 4]
In this example, instead of using *args
, you use a positional argument and pass a list of values to the function call.
You can do a similar thing with **kwargs
and pass a dictionary to the function call:
>>> def show_items(kwvalues):
... print(kwvalues)
...
>>> show_items({"one": 1, "two": 2, "three": 3, "four": 4})
{'one': 1, 'two': 2, 'three': 3, 'four': 4}
In practice, both *args
and **kwargs
are syntactic sugar pieces that Python includes to make your life more pleasant and your code more explicit.
Loops and Comprehensions
Loops are a fundamental component of most programming languages. With a loop, you can run repetitive tasks, process data streams, and more. In Python, you have while
and for
loops. Python also has comprehensions, which are like a compact for
loop.
In the following sections, you’ll learn how for
loops and comprehensions are also syntactic sugar constructs in Python, as they can be rewritten as while
loops.
Exploring for
Loops
A for
loop lets you iterate over the items of a given data stream that you typically call an iterable. Lists, tuples, sets, and dictionaries are good examples of iterables in Python. All of them support iteration, meaning you can use a for
loop to traverse them.
Note: To learn more about loops in Python, check out the following tutorials:
Here’s a toy example of a for
loop:
>>> numbers = ["one", "two", "three", "four"]
>>> for number in numbers:
... print(number)
...
one
two
three
four
This loop iterates over a list of strings and prints one string at a time. You can write a while
loop to replace the above loop:
>>> numbers = ["one", "two", "three", "four"]
>>> index = 0
>>> while index < len(numbers):
... print(numbers[index])
... index += 1
...
one
two
three
four
In this example, you did the same iteration and produced the same result with a while
loop. The code looks less clean and readable, but it works the same. So, you can conclude that for
loops are also syntactic sugar in Python.
Note: In these examples, the loops use only their basic form. They don’t include the else
clause.
The while
loop in the above example works as a replacement for a for
loop as long as you can call len()
on the target iterable. In practice, something like the following is closer to how a for
loop would work. Again, this example doesn’t consider the else
clause of the loop:
>>> it = iter(numbers)
>>> while True:
... try:
... number = next(it)
... except StopIteration:
... break
... print(number)
...
one
two
three
four
To implement this loop, you use the built-in iter()
function that creates an iterator from the input data stream. Inside the loop, you use the built-in next()
function to get the next item from the iterator in every cycle of the loop. Then, you use the StopIteration
exception to terminate the loop with a break
statement.
Using Comprehensions
Comprehensions are a distinctive feature of Python. You can use comprehensions to create new lists, sets, and dictionaries out of a stream of data. To illustrate, say that you have a list of numbers as strings, and you want to convert them into numeric values and build a list of squared values. To do this, you can use the following comprehension:
>>> numbers = ["2", "9", "5", "1", "6"]
>>> [int(number)**2 for number in numbers]
[4, 81, 25, 1, 36]
In this example, you use a list comprehension to iterate over your list of numbers. The comprehension’s expression converts the string values into integer values and computes their squares. As a result, you get a list of square numbers.
Note: To learn more about list comprehensions, check out the When to Use a List Comprehension in Python tutorial.
Even though comprehensions are popular and versatile tools, you can replace them with an equivalent for
loop or even a while
loop. Consider the for
loop approach only:
>>> numbers = ["2", "9", "5", "1", "6"]
>>> squares = []
>>> for number in numbers:
... squares.append(int(number)**2)
...
>>> squares
[4, 81, 25, 1, 36]
This code is more verbose and requires an extra variable to store the list of squares. However, it produces the same result as the equivalent comprehension. Again, comprehensions are syntactic sugar in Python.
As an exercise, you can use what you learned in the previous section to transform the comprehension into a while
loop.
Decorators
Decorators are functions that take another function as an argument and extend their behavior dynamically without explicitly modifying it. In Python, you have a dedicated syntax that allows you to apply a decorator to a given function:
@decorator
def func():
<body>
In this piece of syntax, the @decorator
part tells Python to call decorator()
with the func
object as an argument. This operation lets you modify the original behavior of func()
and assign the function object back to the same name, func
.
Note: To learn more about decorators, check out the Primer on Python Decorators tutorial.
To illustrate the basics of using decorators, say that you need to measure the execution time of a given function. A handy way to do this is to use a decorator that looks something like the following:
>>> import functools
>>> import time
>>> def timer(func):
... @functools.wraps(func)
... def _timer(*args, **kwargs):
... start = time.perf_counter()
... result = func(*args, **kwargs)
... end = time.perf_counter()
... print(f"Execution time: {end - start:.4f} seconds")
... return result
... return _timer
This timer()
function is built to be used as a decorator. It takes a function object as an argument and returns an extended function object. In the ._timer()
inner function, you use the time
module to measure the execution time of the input function.
Here’s how you can use this decorator in your code:
>>> @timer
... def delayed_mean(sample):
... time.sleep(1)
... return sum(sample) / len(sample)
...
>>> delayed_mean([10, 2, 4, 7, 9, 3, 9, 8, 6, 7])
Execution time: 1.0051 seconds
6.5
In this example, you use the @decorator
syntax to decorate the delayed_mean()
function and modify its behavior. Now when you call delayed_mean()
, you get a time report and the expected result.
The fact is that you can get the same result without using the @decorator
syntax. Here’s how:
>>> def delayed_mean(sample):
... time.sleep(1)
... return sum(sample) / len(sample)
...
>>> delayed_mean = timer(delayed_mean)
>>> delayed_mean([10, 2, 4, 7, 9, 3, 9, 8, 6, 7])
Execution time: 1.0051 seconds
6.5
The highlighted line causes the same effect as decorating the function with the @decorator
syntax. After executing this assignment, you can use delayed_mean()
as usual. You’ll get the time report and the computed result.
Attribute Access
In Python, when you’re working with classes and objects, you’ll often use the dot notation to access the attributes of a class or object. In other words, to access class members, you can use the following syntax:
obj.attribute
This syntax uses a dot to express that you need to access attribute
on obj
. This clean and intuitive syntax makes your code look readable and clear. However, it’s also syntactic sugar. You can replace this syntax with an alternative construct based on the built-in getattr()
function.
To illustrate, consider the following Circle
class:
circle.py
from math import pi
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return pi * self.radius**2
def circumference(self):
return 2 * pi * self.radius
In this code, you define the Circle
class with an instance attribute and two methods. To access the attribute and the methods, you can use the dot notation:
>>> from circle import Circle
>>> circle = Circle(10)
>>> circle.radius
10
>>> circle.area()
314.1592653589793
>>> circle.circumference()
62.83185307179586
In this example, you see that the dot notation gives you access to the attributes and methods of a given instance of Circle
. You can also access those members using the following constructs:
>>> getattr(circle, "radius")
10
>>> # Method object
>>> getattr(circle, "area")
<bound method Circle.area of <__main__.Circle object at 0x1106d24d0>>
>>> # Method calls
>>> getattr(circle, "area")()
314.1592653589793
>>> getattr(circle, "circumference")()
62.83185307179586
The built-in getattr()
function lets you access attributes and methods on a given object. Note that when you use this function to access methods, you get the method object. If you want to call the method, then you need to append a pair of parentheses with the required arguments.
Method Calls
There’s another piece of syntactic sugar associated with how you normally call methods on Python objects. To continue with the Circle
class from the previous section, the usual way to call its instance methods is by using the dot notation on an instance:
>>> from circle import Circle
>>> circle = Circle(10)
>>> circle.area()
314.1592653589793
>>> circle.circumference()
62.83185307179586
This way of calling the methods is also syntactic sugar. Internally, doing something like circle.area()
automatically translates to something like the following:
>>> Circle.area(circle)
314.1592653589793
In this example, you explicitly pass the instance to the self
argument on .area()
. However, in something like circle.area()
, Python takes care of setting self
to the provided instance for you. This behavior makes your life easier and your code cleaner.
F-String Literals
Formatted string literals, or f-strings for short, are another piece of syntactic sugar in Python. F-strings are popular in modern Python code. They allow you to interpolate and format strings with a quick and clean syntax.
Note: To learn more about f-strings and other string interpolation and formatting tools, check out the following tutorials:
Here’s a quick example of how to use f-strings in your code:
>>> debit = 300
>>> credit = 450
>>> f"Debit: ${debit:.2f}, Credit: ${credit:.2f}, Balance: ${credit - debit:.2f}"
'Debit: $300.00, Credit: $450.00, Balance: $150.00'
In this example, you use an f-string to create a quick report of a bank account. Note how you use replacement fields to interpolate the debit
and credit
variables and format specifiers to format the output.
Instead of using an f-string, you can use the string .format()
method:
>>> "Debit: ${0:.2f}, Credit: ${1:.2f}, Balance: ${2:.2f}".format(
... debit, credit, credit - debit
... )
'Debit: $300.00, Credit: $450.00, Balance: $150.00'
The .format()
method produces the same result as the equivalent f-string. However, its syntax may be less readable at times. In practice, you can replace any f-string instance with a call to .format()
. So, f-strings are also syntactic sugar in Python.
Alternatively, you can use string concatenation with +
and calls to the built-in format()
function:
>>> (
... "Debit: $" + format(debit, ".2f")
... + ", Credit: $" + format(credit, ".2f")
... + ", Balance: $" + format(credit - debit, ".2f")
... )
'Debit: $300.00, Credit: $450.00, Balance: $150.00'
In this example, you concatenate the different components of your string using the concatenation operator. To format the input values, you use the format()
function. This syntax is quite involved but it works the same as the initial f-string.
Assertions
Python allows you to write sanity checks known as assertions. To write these checks, you’ll use the assert
statement. With assertions, you can test if certain assumptions remain true while developing your code. If any of your assertions are false, then you probably have a bug in your code.
Note: To learn more about assertions, check out Python’s assert: Debug and Test Your Code Like a Pro.
Assertions are mainly used for debugging purposes. They help ensure that you don’t introduce new bugs while adding features and fixing other bugs in your code.
The assert
statement is a simple statement with the following syntax:
assert expression[, assertion_message]
Here, expression
can be any valid Python expression or object, which is then tested for truthiness. If expression
is false, then the statement throws an AssertionError
. The assertion_message
parameter is optional but encouraged. It can hold a string describing the issue the statement should catch.
Here’s how this statement works in practice:
>>> number = 42
>>> assert number > 0, "number must be positive"
>>> number = -42
>>> assert number > 0, "number must be positive"
Traceback (most recent call last):
...
AssertionError: number must be positive
With a truthy expression
, the first assertion succeeds, and nothing happens. In that case, your program continues its normal execution. In contrast, a falsy expression
makes the assertion fail, raising an AssertionError
and breaking the program’s execution.
The assert
statement is also syntactic sugar. You can replace the above assertions with the following code:
>>> number = 42
>>> if __debug__:
... if not number > 0:
... raise AssertionError("number must be positive")
...
>>> number = -42
>>> if __debug__:
... if not number > 0:
... raise AssertionError("number must be positive")
...
Traceback (most recent call last):
...
AssertionError: number must be positive
Python’s built-in constant, __debug__
, is closely related to the assert
statement. It’s a Boolean constant that defaults to True
, which means that the assertions are enabled, and you’re running Python in normal or debug mode.
If you run Python in optimized mode, then the __debug__
constant is False
and the assertions are disabled.
The yield from
Construct
The yield from
construct is another syntactic sugar piece in Python. You can use this construct to yield items from an iterable.
To illustrate how you can use this construct, consider the following Stack
class:
stack.py
class Stack:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def pop(self):
return self.items.pop()
def __iter__(self):
yield from self.items
This Stack
class has two basic stack operations: push and pop. To store the data, you use a list
object called .items
. The .__iter__()
special method enables your class to support iteration. In this example, you use the yield from
construct to yield values from the .items
list. The syntax is highly readable and clear.
You can replace the yield from
construct with at least two different tools. One of these is the built-in iter()
function:
stack.py
class Stack:
# ...
def __iter__(self):
return iter(self.items)
The highlighted line works the same as the yield from
construct in the previous version of .__iter__()
. It returns an iterator that yields items on demand.
Note: In some use cases, it can be hard to replace yield from
with iter()
. Consider the following toy example:
>>> class Twice:
... def __init__(self, items):
... self.items = list(items)
... def __iter__(self):
... yield from self.items
... print("Halfway there!")
... yield from self.items
...
>>> for number in Twice([1, 2, 3]):
... print(f"-> {number}")
...
-> 1
-> 2
-> 3
Halfway there!
-> 1
-> 2
-> 3
In this example, using return iter(self.items)
in the first line of .__iter__()
won’t work because the return
statement will make the rest of the code unreachable.
Alternatively, you can use a for
loop:
stack.py
class Stack:
# ...
def __iter__(self):
for item in self.items:
yield item
In this other implementation of .__iter__()
, you use an explicit for
loop with a plain yield
statement that yields items on demand.
The with
Statement
Python’s with
statement is a handy tool that allows you to manipulate context manager objects. These objects automatically handle the setup and teardown phases whenever you’re dealing with external resources such as files.
For example, to write some text to a file, you’ll typically use the following syntax:
with open("hello.txt", mode="w", encoding="utf-8") as file:
file.write("Hello, World!")
The built-in open()
function returns a file object that supports the context manager protocol. Therefore, you can use this object in a with
statement as shown above.
In the statement’s code block, you can manipulate your file as needed. When you finish the file, the with
statement finishes, closing the file and releasing the associated resources automatically.
Note: To dive deeper into the with
statement, check out the Context Managers and Python’s with Statement tutorial.
The with
statement is another piece of syntactic sugar that makes your life easier when handling setup and teardown logic. You can replace this statement using a try
… finally
statement like the following:
file = open("hello.txt", mode="w", encoding="utf-8")
try:
file.write("Hello, World!")
finally:
file.close()
Here, you first get the file object by calling open()
. Then, you define a try
block to manipulate the file as needed. In the finally
clause, you manually close the file to release the associated resource. This clause always runs, so you can rest assured that the file will be properly closed.
In this example, the only action you do in the finally
clause is close the file. However, if you ever need to use this syntax to replace a with
statement, then you must use the finally
clause to do whatever action the target context manager does in its teardown phase, which is carried out by its .__exit__()
method.
Conclusion
Now you know what syntactic sugar is and how Python uses it. You also know more about the most commonly used pieces of syntactic sugar and how they can help you to write more readable, clean, concise, and secure code.
In this tutorial, you’ve learned:
- What syntactic sugar is
- How syntactic sugar applies to operators
- How assignment expressions are syntactic sugar
- How
for
loops and comprehensions are syntactic sugar - How other Python constructs are syntactic sugar
While using syntactic sugar isn’t something you have to do, incorporating it into your workflow can be a sweet way to make your code more Pythonic.
Get Your Code: Click here to download the free sample code that shows you how to use syntactic sugar in Python.
Take the Quiz: Test your knowledge with our interactive “Syntactic Sugar: Why Python Is Sweet and Pythonic” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Syntactic Sugar: Why Python Is Sweet and PythonicYou can take this quiz to test your understanding of Python's most common pieces of syntactic sugar and how they make your code more Pythonic and readable.