Complete Python Tutorial for Absolute Beginners in 2023

Complete Python Tutorial for Absolute Beginners in 2023

Try this free mega-course if you're willing to put in the work

·

39 min read

Table of contents

Python is an unbelievably powerful programming language that is used by millions of developers in production systems around the world. It's easy to learn, free to use, and has a huge community of developers that are always willing to help.

If you're interested in back-end web development, data science, data engineering, or just want to automate some of your boring tasks, Python is a great place to start.

Would you rather learn by doing?

I've included all the static read-only material you'll need here in this tutorial, but if you would like a more hands-on experience, you can take the interactive version of this course, complete with coding challenges and projects on Boot.dev here.

Chapter 1: Introduction

python

Thousands of students start their coding journey right here with Python. We think it's the best programming language to get started with. Python is famous for being a simple language that's easy to read and write.

However, just because it's simple that doesn't mean it's not useful! Python is an extremely popular language in the industry, and is well-known for:

  • Backend web servers
  • DevOps and cloud engineering
  • Machine learning
  • Scripting and automation
  • etc...

On the other hand, it's not particularly well-known for front-end work. While it's possible to do so, Python isn't typically used to build visual user interfaces.

Setup a Local Development Environment

To get started with Python, you'll need to install the python command on your computer, and then install a text editor or IDE. If you're not already familiar with how to do that, I have a full step-by-step project guide here that you can follow.

If you're able to edit and run Python code on your computer, you're ready to continue!

What is "Code"?

Code is just a series of instructions that computers can follow. Computers obey each instruction, one after another.

Programs can be comprised of many instructions. Like many. Like millions.

Addition is one of the most common instructions in coding.

Printing numbers

print() can print text using quotes:

print("some text here")

but it can also print numbers without quotes:

print(1)

and you can do math directly inside the parentheses:

print(1 + 2)

Try some of these code snippets in your editor! For example, you could create a file called hello.py and write the following code:

print("Hello, world!")
print(1 + 2)

Then, run the file with python hello.py and see what happens.

Multiple Instructions

Code runs in order, starting at the top of the program. For example:

print("this prints first")
print("this prints second")
print("this prints last")

Syntax Errors

Syntax is jargon for "valid code that the computer can understand". For example,

prnt("hello world")

is invalid syntax because prnt() is not a valid function, "print" is spelled incorrectly. As a result, an error will be thrown and the code won't execute.

Syntax varies from langauge to language

A coding language's syntax makes up the rules that define what properly structured expressions and statements look like in that language. For example, in Python, the following would be considered correct syntax:

print("hello world")

While in a different programming language, like Go, the correct syntax would be:

fmt.Println("hello world")

Code can have many different problems that prevent it from working as intended. Some examples include:

  • A bug in the logic. For example, a program that should add numbers multiplies them instead
  • A problem with speed. A program that calculates how to play the perfect game of chess might never be able to finish because it requires too many calculations.
  • A problem with syntax. This is the most common problem for new developers. Luckily the Python interpreter will try to give you a descriptive error message in the console to help you find the problem.

Chapter 2: Variables

Variables are how we store data in our program. So far we've been directly printing data by passing it directly into the print() function.

Now we are going to learn to save the data in variables so we can use and change it before we need to print it.

A variable is a name that we define that will point to some data. For example, I could define a new variable called my_height and set its value to 100. I could also define a variable called my_name and set it equal to "Lane".

Creating variables

To create a new variable in Python we use the following syntax:

my_new_variable_two = 2
this_can_be_called_anything = 3

Variables Vary

Variables are called "variables" because they can hold any value and that value can change (it varies).

For example, the following will print 20:

acceleration = 10
acceleration = 20
print(acceleration)

The line acceleration = 20 reassigns the value of acceleration to 20. It overwrites whatever was being held in the acceleration variable before.

Let's do some math

Now that we know how to store and change the value of variables let's do some math!

Here are some examples of common mathematical operators in Python syntax.

sum = a + b
difference = a - b
product = a * b
quotient = a / b

Comments

Comments don't run like code, they are ignored by the computer. Comments are useful for adding reminders or explaining what a piece of code does in plain English.

Single line comment

# speed is a variable describing how fast your player moves
speed = 2

Multi-line comments (aka docstrings)

You can use triple quotes to start and end multi-line comments as well:

"""
    the code found below 
    will print 'Hello, World!' to the console
"""
print('Hello, World!')

This is useful if you don't want to add the # to the start of each line when writing paragraphs of comments.

Variable Names

Variable names can't have spaces, they're continuous strings of characters.

In Python you should use "snake_case" when creating variable names - it's become the "rule of thumb" for the language. By way of comparison, "camel case" is where the beginning of each new word except the first is capitalized.

No casing (pure insanity)

somevariablehere = 10

Camel Case

someVariableHere = 10

Snake Case

some_variable_here = 10

Basic Variable Types

In Python there are several basic data types.

String Type

"Strings" are raw text in coding speak. They are called "strings" because they are a list of characters strung together. Strings are declared in Python by using single quotes or double quotes. That said, for consistency's sake, we prefer double quotes.

name_with_single_quotes = 'boot.dev'
name_with_double_quotes = "boot.dev"

Numeric Types

Numbers aren't surrounded by quotes when created, but they can have decimals and negative signs.

Integers are numbers without a decimal

x = 5
y = -5

A "Float" is a number with a decimal

x = 5.2
y = -5.2

Boolean Type

A "Boolean" (or "bool") is a type that can only have one of two values: True or False. As you may have heard computers really only use 1's and 0's. These 1's and 0's are just Boolean values.

0 = False
1 = True
is_tall = True

NoneType Variables

Not all variables have a value. We can declare an "empty" variable by setting it to None.

empty = None

The value of empty in this instance is None until we use the assignment operator, =, to give it a value.

None is NOT a specific string

Note that the None type is not the same as a string with a value of "None":

my_none = None # this is a None-type
my_none = "None" # this is a string

Dynamic Typing

Python is dynamically typed. All this means is that a variable can store any type, and that type can change.

For example, if I make a number variable, I can later change that variable to a string:

This is valid:

speed = 5
speed = "five"

Just because you can doesn't mean you should!

In almost all circumstances, it's a bad idea to change the type of a variable. The "proper" thing to do is to just create a new one. For example:

speed = 5
speed_description = "five"

What if it weren't dynamically typed?

Statically typed languages like Go (which you'll learn in a later course) are statically typed instead of dynamically typed. In a statically typed language, if you try to assign a value to a variable of the wrong type, an error would crash the program.

If Python were statically-typed, the first example from before would crash on the second line, speed = "five". The computer would give an error along the lines of you can't assign a string value ("five") to a number variable (speed)

Math With Strings

Most of the math operators we went over earlier don't work with strings, aside from the + addition operator. When working with strings the + operator performs a "concatenation".

"Concatenation" is a fancy word that means the joining of two strings.

first_name = "Lane "
last_name = "Wagner"
full_name = first_name + last_name

full_name now holds the value "Lane Wagner".

Notice the extra space at the end of "Lane " in the first_name variable. That extra space is there to separate the words in the final result: "Lane Wagner".

Multi-Variable Declaration

We can save space when creating many new variables by declaring them on the same line:

sword_name, sword_damage, sword_length = "Excalibur", 10, 200

Which is the same as:

sword_name = "Excalibur"
sword_damage = 10
sword_length = 200

Any number of variables can be declared on the same line, and variables declared on the same line should be related to one another in some way so that the code remains easy to understand.

We call code that's easy to understand "clean code".

Chapter 2: Computing Basics

Python Numbers

In Python, numbers without a decimal part are called Integers - just like they are in mathematics.

Integers are simply whole numbers, positive or negative. For example, 3 and -3 are both examples of integers.

Arithmetic can be performed as you might expect:

Addition

2 + 1
# 3

Subtraction

2 - 1
# 1

Multiplication

2 * 2
# 4

Division

3 / 2
# 1.5 (a float)

This one is actually a bit different - division on two integers will actually produce a float. A float is, as you may have guessed, the number type that allows for decimal values.

Integers

In Python, numbers without a decimal part are called Integers. Contrast this to JavaScript where all numbers are just a Number type.

Integers are simply whole numbers, positive or negative. For example, 3 and -3 are both examples of integers.

Floats

A float is, as you may have guessed, the number type that allows for decimal values.

my_int = 5
my_float = 5.5

Floor division

Python has great out-of-the-box support for mathematical operations. This, among other reasons, is why it has had such success in artificial intelligence, machine learning, and data science applications.

Floor division is like normal division except the result is floored afterward, which means the remainder is removed. As you would expect, this means the result is an integer instead of a float.

7 // 3
# 2 (an integer)

Exponents

Python has built-in support for exponents - something most languages require a math library for.

# reads as "three squared" or
# "three raised to the second power"
3 ** 2
# 9

Changing In Place

It's fairly common to want to change the value of a variable based on its current value.

player_score = 4
player_score = player_score + 1
# player_score now equals 5
player_score = 4
player_score = player_score - 1
# player_score now equals 3

Don't let the fact that the expression player_score = player_score - 1 is not a valid mathematical expression be confusing. It doesn't matter, it is valid code. It's valid because the way the expression should be read in English is:

Assign to player_score the old value of player_score minus 1

Plus Equals

Python makes reassignment easy when doing math. In JavaScript or Go you might be familiar with the ++ syntax for incrementing a number variable. In Python, we use the += operator instead.

star_rating = 4
star_rating += 1
# star_rating is now 5

Scientific Notation

As we covered earlier, a float is a positive or negative number with a fractional part.

You can add the letter e or E followed by a positive or negative integer to specify that you're using scientific notation.

print(16e3)
# Prints 16000.0

print(7.1e-2)
# Prints 0.071

If you're not familiar with scientific notation, it's a way of expressing numbers that are too large or too small to conveniently write normally.

In a nutshell, the number following the e specifies how many places to move the decimal to the right for a positive number, or to the left for a negative number.

Underscores for readability

Python also allows you to represent large numbers in the decimal format using underscores instead of commas to make it easier to read.

num = 16_000
print(num)
# Prints 16000

num = 16_000_000
print(num)
# Prints 16000000

Logical Operators

You're probably familiar with the logical operators AND and OR.

Logical operators deal with boolean values, True and False.

The logical AND operator requires that both inputs are True to return True. The logical OR operator only requires that at least one input is True to return True.

For example:

True AND True = True
True AND False = False
False AND False = False

True OR True = True
True OR False = True
False OR False = False

Python Syntax

print(True and True)
# prints True

print(True or False)
# prints True

Nesting with parentheses

We can nest logical expressions using parentheses.

print((True or False) and False)

First, we evaluate the expression in the parentheses, (True or False). It evaluates to True:

print(True and False)

True and False evaluates to False:

print(False)

So, print((True or False) and False) prints "False" to the console.

Binary Numbers

Binary numbers are just "base 2" numbers. They work the same way as "normal" base 10 numbers, but with 2 symbols instead of 10.

Each 1 in a binary number represents a greater multiple of 2. In a 4-digit number, that means you have the eight's place, the four's place, the two's place, and the one's place. Similar to how in decimal you would have the thousandth's place, the hundredth's place, the ten's place, and the one's place.

  • 0001 = 1
  • 0010 = 2
  • 0011 = 3
  • 0100 = 4
  • 0101 = 5
  • 0110 = 6
  • 0111 = 7
  • 1000 = 8

binary

Bitwise "&" Operator

Bitwise operators are similar to logical operators, but instead of operating on boolean values, they apply the same logic to all the bits in a value. For example, say you had the numbers 5 and 7 represented in binary. You could perform a bitwise AND operation and the result would be 5.

0101 = 5
&
0111 = 7
=
0101 = 5

A 1 in binary is the same as True, while 0 is False. So really a bitwise operation is just a bunch of logical operations that are completed in tandem.

& is the bitwise AND operator in Python. 5 & 7 = 5, while 5 & 2 = 0.

0101 = 5
&
0010 = 2
=
0000 = 0

Binary notation

When writing a number in binary, the prefix 0b is used to indicate that what follows is a binary number.

  • 0b0101 is 5
  • 0b0111 is 7

Example: Guild Permissions

It's common practice in backend development to store user permissions as binary values. Think about it, if I have 4 different permissions a user can have, then I can store that as a 4-digit binary number, and if a certain bit is present, I know the permission is enabled.

Let's pretend we have 4 permissions:

  • can_create_guild - Leftmost bit
  • can_review_guild - Second to left bit
  • can_delete_guild - Second to right bit
  • can_edit_guild - Rightmost bit

Which are represented by 0b0000. For example, if a user only has the can_create_guild permission, their binary permissions would be 0b1000. A user with can_review_guild and can_edit_guild would be 0b0101.

To check for, say, the can_review_guild permission, we can perform a bitwise AND operation on the user's permissions and the enabled can_review_guild bit (0b0100). If the result is 0b0100 again, we know they have that specific permission!

Bitwise "|" Operator

As you may have guessed, the bitwise "or" operator is similar to the bitwise "and" operator in that it works on binary rather than boolean values. However, the bitwise "or" operator "ORs" the bits together. Here's an example:

  • 0101 is 5
  • 0111 is 7
0101
|     
0111
=
0111

A 1 in binary is the same as True, while 0 is False. So a bitwise operation is just a bunch of logical operations that are completed in tandem. When two binary numbers are "OR'ed" together, the result has a 1 in any place where either of the input numbers has a 1 in that place.

| is the bitwise OR operator in Python. 5 | 7 = 7 and 5 | 2 = 7 as well!

0101 = 5
|
0010 = 2
=
0111 = 7

Not

We skipped a very important logical operator - not. The not operator reverses the result. It returns False if the input was True and vice-versa.

print(not True)
# Prints: False

print(not False)
# Prints: True

Chapter 3: Comparisons

Comparison Operators

When coding it's necessary to be able to compare two values. Boolean logic is the name for these kinds of comparison operations that always result in True or False.

The operators:

  • < "less than"
  • > "greater than"
  • <= "less than or equal to"
  • >= "greater than or equal to"
  • == "equal to"
  • != "not equal to"

For example:

5 < 6 # evaluates to True
5 > 6 # evaluates to False
5 >= 6 # evaluates to False
5 <= 6 # evaluates to True
5 == 6 # evaluates to False
5 != 6 # evaluates to True

Evaluations

When a comparison happens, the result of the comparison is just a boolean value, it's either True or False.

Take the following two examples:

is_bigger = 5 > 4
is_bigger = True

In both of the above cases, we're creating a Boolean variable called is_bigger with a value of True.

Since 5 > 4, is_bigger is always assigned the value of True.

Why would I use the comparison if I can just set it to "True"?

You wouldn't in this case. However, let's imagine that instead of hard-coding the numbers 5 and 4, we had some dynamic variables that we don't know the values of. For example, perhaps you're making a video game and need to keep track of player scores.

To calculate who wins, you would need to write something like:

# player_one_points and player_two_points are defined and change somewhere else in the game's code
player_one_wins = player_one_points > player_two_points
print(player_one_wins)

# prints "True" when player one is winning, otherwise prints "False"

Increment / Decrement

If we're changing a number and simply want to increment (add to) or decrement (subtract from) there are special operators for that.

shield_armor = 4
shield_armor += 1
# shield_armor now equals 5
shield_armor += 2
# shield_armor now equals 7
shield_armor = 4
shield_armor -= 1
# shield_armor now equals 3
shield_armor -= 2
# shield_armor now equals 1

Notice that shield_armor+=1 is just short-hand for shield_armor = shield_armor + 1

If Statements

It's often useful to only execute code if a certain condition is met:

if CONDITION:
  # do some stuff here

for example:

if bob_score > bill_score:
  print("Bob Wins!")

If-Else

An if statement can be followed by zero or more elif (which stands for "else if") statements, which can be followed by zero or one else statement. For example:

if score > high_Score:
    print('High score beat!')
elif score > second_highest_score:
    print('You got second place!')
elif score > third_highest_score:
    print('You got third place!')
else:
    print('Better luck next time')

First the if statement is evaluated. If it is True then the if statement's body is executed and all the other elses are ignored.

If the first if is false then the next elif is evaluated. Likewise, if it is True then its body is executed and the rest are ignored.

If none of the if statements evaluate to True then the final else statement will be the only body executed.

If-Else Rules

  • You can't have an elif or an else without an if
  • You can have an else without an elif

Chapter 5: Loops

Loops are a programmer's best friend. Loops allow us to do the same operation multiple times without having to write it explicitly each time.

For example, let's pretend I want to print the numbers 0-9.

I could do this:

print(0)
print(1)
print(2)
print(3)
print(4)
print(5)
print(6)
print(7)
print(8)
print(9)

Even so, it would save me a lot of time typing to use a loop. Especially if I wanted to do the same thing one thousand or one million times.

A "for loop" in Python is written like this:

for i in range(0, 10):
    print(i)

In English, the code says:

  1. Start with i equals 0. (i in range(0)
  2. If i is not less than 10 (range(0, 10)), exit the loop.
  3. Print i to the console. (print(i))
  4. Add 1 to i. (range defaults to incrementing by 1)
  5. Go back to step 2

The result is that the numbers 0-9 are logged to the console in order.

Whitespace matters in Python!

The body of a for-loop must be indented, otherwise you'll get a syntax error.

Example

This code print the numbers 0-9 to the console.

for i in range(0, 10):
    print(i)

Range Continued

The range() function we've been using in our for loops actually has an optional 3rd parameter: the "step".

for i in range(0, 10, 2):
    print(i)
# prints:
# 0
# 2
# 4
# 6
# 8

The "step" parameter determines how much to increment i by in each iteration of the loop. You can even go backwards:

for i in range(3, 0, -1):
    print(i)
# prints:
# 3
# 2
# 1

F-strings in Python

You can create a string with dynamic values by using f-strings in Python. It's a beautiful syntax that I wish more programming languages used.

num_bananas = 10
print(f"You have {num_bananas} bananas")
# You have 10 bananas

The opening quotes need to be proceeded by an f, then any variables within curly brackets have their values interpolated into the string.

Chapter 6: Lists

A natural way to organize and store data is in the form of a List. Some languages call them "arrays", but in Python we just call them lists. Think of all the apps you use and how many of the items in the app are organized into lists.

For example:

  • A twitter feed is a list of posts
  • An online store is a list of products
  • The state of a chess game is a list of moves
  • This list is a list of things that are lists

Lists in Python are declared using square brackets, with commas separating each item:

inventory = ["Iron Breastplate", "Healing Potion", "Leather Scraps"]

Arrays can contain items of any data type, in our example above we have a List of strings.

Vertical syntax

Sometimes when we're manually creating lists it can be hard to read if all the items are on the same line of code. We can declare the array using multiple lines if we want to:

flower_types = [
    "daffodil",
    "rose",
    "chrysanthemum"
]

Keep in mind this is just a styling change. The code will run correctly either way.

Counting in Programming

In the world of programming, counting is a bit strange!

We don't start counting at 1, we start at 0 instead.

Indexes

Each item in an array has an index that refers to its spot in the array.

Take the following array as an example:

names = ["Bob", "Lane", "Alice", "Breanna"]
  • Index 0: Bob
  • Index 1: Lane
  • Index 2: Alice
  • Index 3: Breanna

Indexing into Lists

Now that we know how to create new lists, we need to know how to access specific items in the list.

We access items in a list directly by using their index. Indexes start at 0 (the first item) and increment by one with each successive item. The syntax is as follows:

best_languages = ["JavaScript", "Go", "Rust", "Python", "C"]
print(best_languages[1])
# prints "Go", because index 1 was provided

List length

The length of a List can be calculated using the len() function. Again, we'll cover functions in detail later, but this is the syntax:

fruits = ["apple", "banana", "pear"]
length = len(fruits)
# Prints: 3

The length of the list is equal to the number of items present. Don't be fooled by the fact that the length is not equal to the index of the last element, in fact it will always be one greater.

List Updates

We can also change the item that exists at a given index. For example, we can change Leather to Leather Armor in the inventory array in the following way:

inventory = ["Leather", "Healing Potion", "Iron Ore"]
inventory[0] = "Leather Armor"
# inventory: ['Leather Armor', 'Healing Potion', 'Iron Ore']

Appending in Python

It's common to create an empty list then fill it with values using a loop. We can add values to the end of a list using the .append() method:

cards = []
cards.append("nvidia")
cards.append("amd")
# the cards list is now ['nvidia', 'amd']

Pop Values

.pop() is the opposite of .append(). Pop removes the last element from the array and returns it for use. For example:

vegetables = ["broccoli", "cabbage", "kale", "tomato"];
last_vegetable = vegetables.pop()
# vegetables = ['broccoli', 'cabbage', 'kale']
# last_vegetable = 'tomato'

Counting the items in a list

Remember that we can iterate (count) over all the items in an array using a loop. For example, the following code will print each item in the sports array.

for i in range(0, len(sports)):
    print(sports[i])

No-index Syntax

In my opinion, Python has the most elegant syntax for iterating directly over the items in a list without worrying about index numbers. If you don't need the index number you can use the following syntax:

trees = ['oak', 'pine', 'maple']
for tree in trees:
  print(tree)
# Prints:
# oak
# pine
# maple

tree, the variable declared using the in keyword, directly accesses the value in the array rather than the index of the value. If we don't need to update the item, and only need to access its value then this is a more clean way to write the code.

Find an item in a list

Example of "no-index" or "no-range" syntax:

for fruit in fruits:
    print(fruit)

Modulo operator in Python

The modulo operator can be used to find a remainder:

For example, 7 modulo 2 would be 1, because 2 can be multiplied evenly into 7 at most 3 times:

2 * 3 = 6

Then there is 1 remaining to get from 6 to 7.

7 - 6 = 1

The d operator is the percent sign: %. It's important to recognize modulo is not a percentage though! That's just the symbol we're using.

remainder = 8 % 3
# remainder = 2

An odd number is a number that when divided by 2, the remainder is not 0.

Slicing lists

Python makes it easy to slice and dice lists to work only with the section you care about. One way to do this is to use the simple slicing operator, which is just a colon :.

With this operator, you can specify where to start and end the slice, and how to step through the original. List slicing returns a new list from the existing list.

The syntax is as follows:

Lst[ Initial : End : IndexJump ]
scores = [50, 70, 30, 20, 90, 10, 50]
# Display list
print(scores[1:5:2])
# Prints [70, 20]

The above reads as "give me a slice of the scores list from index 1, up to but not including 5, skipping every 2nd value. All of the sections are optional.

scores = [50, 70, 30, 20, 90, 10, 50]
# Display list
print(scores[1:5])
# Prints [70, 30, 20, 90]
scores = [50, 70, 30, 20, 90, 10, 50]
# Display list
print(scores[1:])
# Prints [70, 30, 20, 90, 10, 50]

List Operations - Concatenate

Concatenating two lists (smushing them together) is really easy in Python, just use the + operator.

all = [1, 2, 3] + [4, 5, 6]
print(all)
# Prints: [1, 2, 3, 4, 5, 6]

List Operations - Contains

Checking whether a value exists in a list is also really easy in Python, just use the in keyword.

fruits = ["apple", "orange", "banana"]
print("banana" in fruits)
# Prints: True

Tip: Quotes within quotes

To use quotes within quotes, they either need to be escaped or you need to use the other kind of quotes. Because we usually use double quotes, we can nest strings with single quotes:

f"banana is in fruits list: {'banana' in fruits}"

List deletion

Python has a built-in keyword del that deletes items from objects. In the case of a list, you can delete specific indexes or entire slices.

nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]

# delete the fourth item
del nums[3]
print(nums)
# Output: [1, 2, 3, 5, 6, 7, 8, 9]

# delete items from 2nd to 3rd
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]
del nums[1:3]
print(nums)
# Output: [1, 4, 5, 6, 7, 8, 9]

# delete all elements
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]
del nums[:]
print(nums)
# Output: []

Tuples

Tuples are collections of data that are ordered and unchangeable. You can think of a tuple as a List with a fixed size. Tuples are created with round brackets:

my_tuple = ("this is a tuple", 45, True)
print(my_tuple[0])
# this is a tuple
print(my_tuple[1])
# 45
print(my_tuple[2])
# True

While it's typically considered bad practice to store items of different types in a List it's not a problem with Tuples. Because they have a fixed size, it's easy to keep track of which indexes store which types of data.

Tuples are often used to store very small groups (like 2 or 3 items) of data. For example, you might use a tuple to store a dog's name and age.

dog = ("Fido", 4)

Because Tuples hold their data, multiple tuples can be stored within a list. Similar to storing other data in lists, each tuple within the list is separated by a comma.

my_tuples = [("this is the first tuple in the list", 45, True),("this is the second tuple in the list", 21, False)]
print(my_tuples[0][0])
# this is the first tuple in the list

Chapter 7: Functions

Functions allow us to reuse and organize code. For example, let's pretend we need to calculate the area of a circle. We can use the formula area = pi * r^2, or in code:

r = 5
area = 3.14 * r * r

This works great! The problem arises when multiple places in our code need to get the area of a circle

r = 5
area1 = 3.14 * r * r

r2 = 7
area2 = 3.14 * r2 * r2

r3 = 11
area3 = 3.14 * r3 * r3

We want to use the same code, why repeat the work?

Let's declare a new function area_of_circle(). Notice that the def keyword is written before the function name, and tells the computer that we're declaring, or defining, a new function.

def area_of_circle(r):
    return 3.14 * r * r

The area_of_circle function takes one input (which can also be called a parameter or argument), and returns a single output. We give our function the radius of a circle and we get back the area of that circle!

To use or "call" the function we can pass in any number as the input, and capture the output into a new variable:

radius = 5
area = area_of_circle(radius)

Let's talk through this code example step by step.

  1. The radius variable is created with a value of 5.
  2. The area_of_circle function is called with a single argument: radius
  3. The area_of_circle function is executed, with r being equal to 5
  4. The result of 3.14 * r * r is returned from area_of_circle, which happens to be 78.75
  5. area_of_circle(radius) resolves to the returned value of 78.75
  6. The area variable is created with a value of 78.75

Multiple Parameters

Functions can have multiple parameters, or inputs:

def subtract(a, b):
    return a - b

Where to Declare Functions

You've probably noticed that a variable needs to be declared before it's used. For example, the following doesn't work:

print(my_name)
my_name = 'Lane Wagner'

It needs to be:

my_name = 'Lane Wagner'
print(my_name)

Lines of code execute in order from top to bottom, so a variable needs to be created before it can be used. That means that if you define a function, you can't call that function until after the definition.

The main() function is a convention used in many programming languages to specify the entrypoint of an application. By defining a single main function, and only calling main() at the end of the entire program we ensure that all of our function are defined before they're called.

Order of functions

All functions must be defined before they're used.

You might think this would make structuring Python code difficult because the order in which the functions are declared can quickly become so dependent on each other that writing anything becomes impossible.

As it turns out, most Python developers solve this problem by simply defining all the functions first, then finally calling the entrypoint function last. If you do that, then the order that the functions are declared in doesn't matter. The entrypoint function is usually called "main".

def main():
    func2()

def func2():
    func3()

def func3():
    print("I'm function 3")

main() # entrypoint

Scope

Scope refers to where a variable or function name is available to be used. For example, when we create variables in a function (by giving names to our parameters for example), that data is not available outside of that function.

For example:

def subtract(x, y)
    return x - y
result = subtract(5, 3)
print(x)
# ERROR! "name 'x' is not defined"

When the subtract function is called, we assign the variable x to 5, but x only exists in the code within the subtract function. If we try to print x outside of that function then we won't get a result, in fact we'll get a big fat error.

Global Scope

So far we've been working in the global scope. That means that when we define a variable or a function, that name is accessible in every other place in our program, even within other functions.

For example:

pi = 3.14

def get_area_of_circle(radius):
    return pi * radius * radius

Because pi was declared in the parent "global" scope, it is usable within the get_area_of_circle() function.

Infinity

The built-in float() function can be used to create a numeric floating point value that represents the negative infinity value. I've added it for you as a starting point.

negative_infinity = float('-inf')
positive_infinity = float('inf')

None Return

When no return value is specified in a function, (for example, maybe it's a function that prints some text to the console, but doesn't explicitly return a value) it will return None. The following code snippets all return exactly the same thing:

def my_func():
    print("I do nothing")
    return None
def my_func():
    print("I do nothing")
    return
def my_func():
    print("I do nothing")

Parameters vs arguments

Parameters are the names used for inputs when defining a function. Arguments are the names of the inputs supplied when a function is called.

To reiterate, arguments are the actual values that go into the function, say 42.0, "the dark knight", or True. Parameters are the names we use in the function definition to refer to those values, which at the time of writing the function, could be anything.

That said, it is important to understand that this is all semantics, and frankly developers are really lazy with these definitions. You'll often hear the words arguments and parameters used interchangeably.

# a and b are parameters
def add(a, b)
    return a + b

# 5 and 6 are arguments
sum = add(5, 6)

Multiple return values

In Python, we can return more than one value from a function. All we need to do is separate each value by a comma.

# returns email, age, and status of the user
def get_user():
    return "name@domain.com", 21, "active"

email, age, status = get_user()
print(email, age, status)
# Prints: "name@domain.com 21 active"
def get_user():
    return "name@domain.com", 21, "active"

# this works, and by convention you should NOT use the underscore variable later
email, _, _ = get_user()
print(email)
# Prints: "name@domain.com"
print(_)
# Prints: "active"

Default values for function arguments

Python has a way to specify a default value for function arguments. This can be convenient if a function has arguments that are essentially "optional", and you as the function creator want to use a specific default value in case the caller doesn't provide one

A default value is created by using the assignment (=) operator in the function signature.

def get_greeting(email, name="there"):
    return f"Hello {name}, welcome! You've registered your email: {email}"
msg = get_greeting("lane@example.com", "Lane")
# Hello Lane, welcome! You've registered your email: lane@example.com
msg = get_greeting("lane@example.com")
# Hello there, welcome! You've registered your email: lane@example.com

If the second parameter is omitted, the default "there" value will be used in its place. As you may have guessed, for this structure to work, optional arguments that have defaults specified come after all the required arguments.

Chapter 8: Dictionaries

Dictionaries in Python are used to store data values in key -> value pairs. Dictionaries are a great way to store groups of information.

car = {
  "brand": "Tesla",
  "model": "3",
  "year": 2019
}

Duplicate keys

Because dictionaries rely on unique keys, you can't have two of the same key in the same dictionary. If you try to use the same key twice, the associated value will simply be overwritten.

Accessing Dictionary Values

Dictionary elements must be accessible somehow in code, otherwise they wouldn't be very useful.

A value is retrieved from a dictionary by specifying its corresponding key in square brackets. The syntax looks similar to indexing into a list.

car = {
    'make': 'tesla',
    'model': '3'
}
print(car['make'])
# Prints: tesla

Setting Dictionary Values

You don't need to create a dictionary with values already inside. It is common to create a blank dictionary then populate it later using dynamic values. The syntax is the same as getting data out of a key, just use the assignment operator (=) to give that key a value.

names = ["jack bronson", "jill mcarty", "john denver"]

names_dict = {}
for name in names:
    # .split() returns a list of strings
    # where each string is a single word from the original
    names_arr = name.split()

    # here we update the dictionary
    names_dict[names_arr[0]] = names_arr[1]

print(names_dict)
# Prints: {'jack': 'bronson', 'jill': 'mcarty', 'john': 'denver'}

Updating Dictionary Values

If you try to set the value of a key that already exists, you'll end up just updating the value of that key.

names = ["jack bronson", "james mcarty", "john denver"]

names_dict = {}
for name in names:
    # .split() returns a list of strings
    # where each string is a single word from the original
    names_arr = name.split()

    # we're always setting the "jack" key
    names_dict["jack"] = names_arr[1]

print(names_dict)
# Prints: {'jack': 'denver'}

Deleting Dictionary Values

You can delete existing keys using the del keyword.

names_dict = {
    'jack': 'bronson',
    'jill': 'mcarty',
    'joe': 'denver'
}

del names_dict['joe']

print(names_dict)
# Prints: {'jack': 'bronson', 'jill': 'mcarty'}

Deleting keys that don't exist

Notice that if you try to delete a key that doesn't exist, you'll get an error.

names_dict = {
    'jack': 'bronson',
    'jill': 'mcarty',
    'joe': 'denver'
}

del names_dict['unknown']
# ERROR HERE, key doesn't exist

Checking for existence

If you're unsure whether or not a key exists in a dictionary, use the in keyword.

cars = {
    'ford': 'f150',
    'tesla': '3'
}

print('ford' in cars)
# Prints: True

print('gmc' in cars)
# Prints: False

Iterating over a dictionary in Python

fruit_sizes = {
    "apple": "small",
    "banana": "large",
    "grape": "tiny"
}

for name in fruit_sizes:
    size = fruit_sizes[name]
    print(f"name: {name}, size: {size}")

# name: apple, size: small
# name: banana, size: large
# name: grape, size: tiny

Ordered or Unordered?

As of Python version 3.7, dictionaries are ordered. In Python 3.6 and earlier, dictionaries were unordered.

Because dictionaries are ordered, the items have a defined order, and that order will not change.

Unordered means that the items used to not have a defined order, so you couldn't refer to an item by using an index.

**The takeaway is that if you're on Python 3.7 or later, you'll be able to iterate over dictionaries in the same order every time.

Chapter 9: Sets

Sets are like Lists, but they are unordered and they guarantee uniqueness. There can be no two of the same value in a set.

fruits = {'apple', 'banana', 'grape'}
print(type(fruits))
# Prints: <class 'set'>

print(fruits)
# Prints: {'banana', 'grape', 'apple'}

Adding values to a set

fruits = {'apple', 'banana', 'grape'}
fruits.add('pear')
print(fruits)
# Prints: {'banana', 'grape', 'pear', 'apple'}

Empty set

Because the {} syntax creates an empty dictionary, to create an empty set, just use the set() function.

fruits = set()
fruits.add('pear')
print(fruits)
# Prints: {'pear'}

Iterate over values in a set (order is not guaranteed)

fruits = {'apple', 'banana', 'grape'}
for fruit in fruits:
    print(fruit)
    # Prints:
    # banana
    # grape
    # apple

Removing values from a set

fruits = {'apple', 'banana', 'grape'}
fruits.remove('apple')
print(fruits)
# Prints: {'banana', 'grape'}

Chapter 10: Errors

You've probably encountered some errors in your code from time to time if you've gotten this far in the course. In Python, there are two main kinds of distinguishable errors.

  • syntax errors
  • exceptions

Syntax errors

You probably know what these are by now. A syntax error is just the Python interpreter telling you that your code isn't adhering to proper Python syntax.

this is not valid code, so it will error

If I try to run that sentence as if it were valid code I'll get a syntax error:

this is not valid code, so it will error
      ^
SyntaxError: invalid syntax

Exceptions

Even if your code has the right syntax however, it may still cause an error when an attempt is made to execute it. Errors detected during execution are called "exceptions" and can be handled gracefully by your code. You can even raise your own exceptions when bad things happen in your code.

Python uses a try-except pattern for handling errors.

try:
  10 / 0
except Exception as e:
  print(e)

# prints "division by zero"

The try block is executed until an exception is raised or it completes, whichever happens first. In this case, a "divide by zero" error is raised because division by zero is impossible. The except block is only executed if an exception is raised in the try block. It then exposes the exception as data (e in our case) so that the program can handle the exception gracefully without crashing.

Raising your own exceptions

Errors are not something to be scared of. Every program that runs in production is expected to manage errors on a constant basis. Our job as developers is to handle the errors gracefully and in a way that aligns with our user's expectations.

Errors are NOT bugs

When something in our own code happens that we don't expect, we should raise our own exceptions. For example, if someone passes some bad inputs to a function we write, we should not be afraid to raise an exception to let them know they did something wrong.

An error or exception is raised when something bad happens, but as long as our code handles it as users expect it to, it's not a bug. A bug is when code behaves in ways our users don't expect it to.

For example, if a player tries to forge an iron sword out of bronze metal, we might raise an exception and display an error message to the player. However, that's the expected behavior of the game, so it's not a bug. If a player can forge the iron sword out of bronze, that may be considered a bug because that's against the rules of the game.

raise Exception("something bad happened")

Software applications aren't perfect, and user input and network connectivity are far from predictable. Despite intensive debugging and unit testing, applications will still have failure cases.

Loss of network connectivity, missing database rows, out of memory issues, and unexpected user inputs can all prevent an application from performing "normally". It is your job to catch and handle any and all exceptions gracefully so that your app keeps working. When you are able to detect that something is amiss, you should be raising the errors yourself, in addition to the "default" exceptions that the Python interpreter will raise.

raise Exception("something bad happened")

#Different types of exceptions

We haven't covered classes and objects yet, which is what an Exception really is at its core. We'll go more into that in the object-oriented programming course that we have lined up for you next.

For now, what is important to understand is that there are different types of exceptions and that we can differentiate between them in our code.

More syntax for errors

try:
    10/0
except ZeroDivisionError:
    print("0 division")
except Exception:
    print("unknown exception")

try:
    nums = [0, 1]
    print(nums[2])
except ZeroDivisionError:
    print("0 division")
except Exception:
    print("unknown exception")

Which will print:

0 division
unknown exception

Chapter 11: Python Facts

The Zen of Python

Tim Peters, a long time Pythonista describes the guiding principles of Python in his famous short piece, The Zen of Python.

Beautiful is better than ugly.

Explicit is better than implicit.

Simple is better than complex.

Complex is better than complicated.

Flat is better than nested.

Sparse is better than dense.

Readability counts.

Special cases aren't special enough to break the rules.

Although practicality beats purity.

Errors should never pass silently.

Unless explicitly silenced.

In the face of ambiguity, refuse the temptation to guess.

There should be one-- and preferably only one --obvious way to do it.

Although that way may not be obvious at first unless you're Dutch.

Now is better than never.

Although never is often better than right now.

If the implementation is hard to explain, it's a bad idea.

If the implementation is easy to explain, it may be a good idea.

Namespaces are one honking great idea -- let's do more of those!

Why Python?

Here are some reasons we think Python is a future-proof choice for developers:

  • Easy to read and write - Python reads like plain English. Due to its simple syntax, it's a great choice for implementing advanced concepts like AI. This is arguably Python's best feature.
  • Popular - According to the Stack Overflow Developer Survey, Python is the 4th most popular coding language in 2020.
  • Free - Python, like many languages nowadays, is developed under an open-source license. It's free to install, use, and distribute.
  • Portable - Python written for one platform will work on any other platform.
  • Interpreted - Code can be executed as soon as it's written. Because it doesn't need to take a long time to compile like Java, C++, or Rust, releasing code to production is typically faster.

Why not Python?

Python might not be the best choice for a project if:

  • The code needs to run fast. Python code executes very slowly, which is why performance critical applications like PC games aren't written in Python.
  • The codebase will become large and complex. Due to its dynamic type system, Python code can be harder to keep clean of bugs.
  • The application needs to be distributed directly to non-technical users. They would have to install Python in order to run your code, which would be a huge inconvenience.

Python 2 vs Python 3

One thing that's important to keep in mind as you continue your Python journey is that the Python ecosystem suffers from split personality syndrome. Python 3 was released on December 3rd, 2008, but over a decade later the web is still full of Python 2 dependencies, scripts and tutorials.

In this course, we used Python 3 - just like any good citizen should these days.

One of the most obvious breaking changes between Python 2 and 3 is the syntax for printing text to the console.

Python 2

print "hello world"

Python 3

print("hello world")

Congratulations on making it to the end!

If you're interested in doing the interactive coding assignments and quizzes for this course you can check out the Learn Python course over on Boot.dev.

That course is a part of my full back-end developer career path, made up of other courses and projects if you're interested in checking those out.