Why Python Is Sweet and Pythonic – Real Python
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.
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:
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.
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:
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:
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.
To illustrate, here’s how you can express the arithmetic operations you wrote earlier using the appropriate special methods:
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:
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:
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:
With these methods, you can create constructs that work like usual comparison expressions. Consider the following examples of expressions and their equivalent method calls:
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:
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
.
In practice, you can replace these operators with calls to the .__contains__()
method:
Alternatively, you can implement the algorithm in a function that iterates over the values in the target iterable:
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:
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:
Now, your condition doesn’t explicitly include the and
operator. However, it produces the same result. So, both conditions are equivalent.
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:
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:
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"
:
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"
.
In practice, you rewrite this loop without using the walrus operator by lifting the variable assignment:
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:
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:
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:
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:
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:
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:
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.
On the other hand, the **kwargs
argument packs keyword arguments into a dictionary:
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:
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:
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:
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:
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:
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:
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:
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.
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:
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:
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
.
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:
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:
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:
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:
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:
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:
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:
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:
This way of calling the methods is also syntactic sugar. Internally, doing something like circle.area()
automatically translates to something like the following:
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:
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:
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:
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.
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:
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:
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:
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:
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:
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:
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:
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:
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.
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:
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.
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: