Class 0: Background and Basic Syntax#

Introduction to Python#

Foreword#

What is this course all about?#

  1. Learning to program with a fully-featured programming language.

    • Use professional programming tools.

    • Write high-quality software.

    • Use software development standards.

  2. Python!

    • All of the basics.

    • Some more advanced stuff.

    • Important tools and packages for (neuroscientific) research.

    • Correct programming habits.

  3. Useful scripting.

    • Creating reliable, robust, data analysis pipelines.

    • Code to make your science reproducible and correct.

      • You might find me insisting on seemingly unimportant points.

    • Making you comfortable with learning other programming languages.

What’s in store for this class?#

  • What’s Python?

  • Why Python?

  • Who’s behind Python?

  • Syntax

  • Basic data structures

What can you do with Python?#

antigravity

  • 99% of what MATLAB does, and often better

    • Linear algebra

    • Signal and image processing

    • Data analysis

    • Visualizations

    • Graphical User Interfaces (GUIs)

  • Data science

  • Full-stack Web development

  • Machine learning

  • Much more…

Why Python?#

  • Open source software

  • Fully-featured language

  • Widely adopted

  • Easy to use but also allows for advanced programming paradigms

  • In continuous development

  • Active and accessible community

Who’s behind Python?#

  • Python was created in the early 90’s by Guido van Russom.

  • In 2018 he left the role of Benevolent Dictator for Life (BDFL), and now Python is governed by a selected board of Core Developers.

  • Changes in the core language are the result of a standardized Python Enhancement Proposals (PEP) process.

  • Hundreds of thousands of libraries created by 3rd party developers like us.

  • Python users are the ones developing the language, fixing bugs, creating new features, etc.

Python 2 vs. Python 3#

If you’ve googled Python you probably read something about Python 2 and Python 3.

  • In short, Python 3 is a non-backwards-compatible upgrade to Python 2, and was released in the end of 2008.

  • The advantages of Python 3 are many-fold, and we’ll be using the latest version of Python 3, namely Python 3.9 - 3.11, in our course.

  • I strongly advise you to use Python 3 if you’re not shackled to old Python codebases. Python 2 is depracated and you should not rely on any code written in it.

The Basics#

How does Python work internally?#

Python is an interpreted language, like MATLAB. This means that you don’t have to compile your code to make it run - the Python interpreter does that for you.

The interpreter goes (generally) line-by-line, parsing your expressions and statements to produce C code, a process known as transpilation. The C code is eventually compiled into machine code that is then fed into the processor.

The interpreter can parse either code we write in an editor, or code that was written in a command prompt:

Python REPL vs. Editor

Again, this is quite similar to MATLAB:

MATLAB REPL vs. Editor

As you see, we have no direct access to the C code that the Python interpreter makes, nor do we really care about it. From the programmer’s point of view writing Python is a very straight-forward process.

For a more detailed comparison of Python and MATLAB, see this transition guide as well as this article.

Whitespaces and Brackets#

Whitespaces in the beginning of lines are important in Python, since they symbolize the start of a new scope, or a new area in the source code (like a function or an if statement. Here are two lines of simple Python code:

>>> a = 2 + 2
>>>  print(a)

Sadly, this code will not compile (i.e. run) due to the single whitespace before the print statement. There’s no reason for this statement to be in a different scope, and so Python doesn’t allow it.

However, this is an example of a proper use of scopes:

>>> for i in [1, 2, 3]:
...     print(i)

You probably noticed the indentation before the print statement, which is necessary here for this code to run. In Python you may indent code with any number of spaces or tabs, but nearly everyone uses 4-space indentation using spaces only (no tabs). Every editor can be configured to do that for you.

The colon (:) indicates that the following line should be indented.

You may have also noticed that Python is wary of brackets and doesn’t use end statements either. An end of a scope is simply marked by unindenting the next line of code. It might seem like a dramatic difference from your programming language of choice, but you’ll see it becomes trivial in no time.

# This is a comment - we'll use it a lot!

# Example of whitespaces and (no) brackets in Python code
a = 1
if a > 1:  # the colon (:) requires us to indent the next line
    b = 2
else:  # the 'else' statement is in the same scope as the original 'if'
    c = 4
print("I'm out of Scope!")  # scope ended by unindenting
I'm out of Scope!

General Syntax#

Before diving into all the different variable types and how to use them, let’s get a general feel for Python’s basic syntax.

Numbers:

2 + 3
5
3 / 2
1.5
# Exponentiation (**)
2 ** (3 + 2)
32
# Floor devision
11 // 4
2
# Modulus operator
11 % 4
3
# Print stuff to screen
print(1 + 2)
print(1, 5, (3+2))  # separate arguments for the print() function are printed with spaces
3
1 5 5

Booleans:

# Boolean logic - capital letter
True
True
False
False
1 == 2
False
2 < 4
True
2 <= 3
True
# Here's something cool
10 < 12 < 14
True
5 != 2  # !=, not ~=
True

Strings:

'This is a string'
'This is a string'
"This is a string as well - we're identical in all possible ways (but look at that apostrophe!)"
"This is a string as well - we're identical in all possible ways (but look at that apostrophe!)"
"""A
multi
line
string
"""
'A\nmulti\nline\nstring\n'
'''
Multi-line strings can be used as
multi-line comments
'''
'\nMulti-line strings can be used as\nmulti-line comments\n'

Types#

Types and data structures are essential in all programming languages. The type of any given new variable you create is the first decision you make about it (be it explicitly or implicitly), and it is an important one.

Python has many types that you might have not heard of if you’ve only used MATLAB, but once you will get to know and use them you will not be able to remember how you’ve written code without them.

# Unlike MATLAB
print(type(42))

# Unlimited length integers
print(2**64 + 1)
<class 'int'>
18446744073709551617
type(1_000_000)  # easier to read
int
type(42.0)
type(4.2)
type(.4)
type(2.)
type(4e2)
float
print(type('42.0'))
# String are unicode characters, so this works as well:
print('\u0BF8')  # Tamil language
print("עברית")
print('\u0C1C\u0C4D\u0C1E\u200C\u0C3E')  # sequence of unicode characters that crashed iOS 10 :)
print('🐍')  # Emojis are strings as well
<class 'str'>
௸
עברית
జ్ఞ‌ా
🐍
Type Coersion#
type(1 + 2)
int
type(3 - 4)
int
type(1 + 2.)
float
type(4 / 2)
float
type(5 // 2)
int
type(11 % 4.2)
float

Variables#

  • No need to declare the type in advance (just like MATLAB).

  • Variables can change types (“dynamically typed”).

  • No (day-to-day) memory concerns (garbage collected).

Naming#
a = 42
b4 = 4.
# 4b = 'a'  # not allowed! variable names can contain numbers, but not start with one
a_message = 'This variable is a string variable.'
a_message = 9  # legal
# class = 1  # doesn't work! Reserved keyword

Note

For a list of Python 3 keywords see the Keywords section in the official Python documentation.

In Python variables are written in snake_case, not PascalCase or camelCase. Generally speaking, the Python core developers defined how Python code should look in PEP8. I strongly encourage to take a look and at least get a feel for how Python code is meant to be written.

In this course we will try and develop skills and habits that will enable us to write code that is usable and maintainable both by us and by our colleagues. Writing code that conforms with the standard styling instructions reduces cognitive load and facilitates efficient communication between developers (and often between ourselves in the present and ourselves in the past).

jUst Imaagin IF i sTarTed makING up my o_wn ?????, for writING.natural.language text!` ~~ ReadING dis b00k - would B,come nigHTmare[!]

Poorly written Python code doesn’t feel much different, only worse.

String operations#

  • Everything in Python is an object, which means we can do stuff like:

"a" + "b"
'ab'
'444' * 3
'444444444'
a = 3
b = "Duck"
print(a * b)
DuckDuckDuck

Division and subtraction aren’t defined for strings.

Functions#

  • We’ve already seen a couple of functions:

    • type()

    • print()

Here are a few more built-in functions:

int(3.9)
3
float('-3')
-3.0
# But
float("Hello, world!")
# Notice the "ValueError" exception we received, one of many exceptions we'll encounter...
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[35], line 2
      1 # But
----> 2 float("Hello, world!")
      3 # Notice the "ValueError" exception we received, one of many exceptions we'll encounter...

ValueError: could not convert string to float: 'Hello, world!'
str(100)
'100'

Defining Functions#

def multiprint(statement, repetitions):
    """ Prints the *statement* multiplied by *repetitions* """
    multistatement = statement * repetitions
    print(multistatement)
    return multistatement
Things to notice:#
  • def is a reserved keyword.

  • Indentation - no brackets, no end, just colon (:) followed by 4 spaces.

  • Docstring: """ The place to document what this function does - its inputs, its logic and its outcome.

  • snake_case naming.

  • Returning is optional - you don’t have to add the return statement.

Functions can have also zero arguments:

def always_return_2():  # numbers in function names (again, not at the start)
    return 2
# Calling a function is done with parenthesis
always_return_2
<function __main__.always_return_2()>
always_return_2()
2

If you want to return more than one item for a function, wrap it in a tuple.

def return_two_items():
    return (1, 2)

Nested Functions

Try not to define functions within other functions. While it’s technically possible, it’s almost never the right thing to do.

Data Structures#

Lists, Tuples, Dictionaries and Sets#

Python has several built-in data containers. You should familiarize yourself with them, since each of them has its own use cases.

Lists#

As basic as they are important:

a_list = [1, 2, 3]  # square brackets
print(a_list)
[1, 2, 3]
another_list = ["a", "b", "c"]
l = [1, "2", '3', 4.0, True, always_return_2]  # heterogeneous
l.append(-100)  # mutable
print(l)
[1, '2', '3', 4.0, True, <function always_return_2 at 0x7fc45c778820>, -100]
# The length of a list (and most other Python objects) is received from the len() function
len(l)
7
# Lists are like arrays
# 1D array
oned = [1, 2, 3]

# 2D array
twod = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Slicing#

Many data structures support indexing:

Slicing

  • Starts from 0, ends at n-1.

  • Inclusive-exclusive (start-end).

  • Negative indices are valid.

  • Overall - better than 1-based indexing, but takes time getting used to.

# Slicing (indexing) into a list
print("The original list:")
print(l)

print("l[0]:", l[0])

print("l[5]:", l[5])

print("l[0:2]:", l[0:2])
The original list:
[1, '2', '3', 4.0, True, <function always_return_2 at 0x7fc45c778820>, -100]
l[0]: 1
l[5]: <function always_return_2 at 0x7fc45c778820>
l[0:2]: [1, '2']
# l = [1, '2', '3', 4.0, True, alwyas_return_2, -100]
print(l[0], '-----', l[5])
1 ----- <function always_return_2 at 0x7fc45c778820>
# l = [1, '2', '3', 4.0, True, always_return_2, -100]
print(l[-1], l[-3])
-100 True
# l = [1, '2', '3', 4.0, True, always_return_2, -100]
l[1:3]  # inclusive:exclusive
['2', '3']
# l = [1, '2', '3', 4.0, True, always_return_2, -100]
l[1:-1:2]  # start : stop : step
['2', 4.0, <function __main__.always_return_2()>]
# l = [1, '2', '3', 4.0, True, always_return_2, -100]
l[-1:1:-2]  # stepping backwards
[-100, True, '3']
# l = [1, '2', '3', 4.0, True, always_return_2, -100]
# Skipping the ending index tells Python to run till the end
l[1::2]
['2', 4.0, <function __main__.always_return_2()>]
# l = [1, '2', '3', 4.0, True, always_return_2, -100]
# beware - MIND BLOWN ALERT
print(l[::-1])
[-100, <function always_return_2 at 0x7fc45c778820>, True, 4.0, '3', '2', 1]
l.pop(2)
print(l)
[1, '2', 4.0, True, <function always_return_2 at 0x7fc45c778820>, -100]
List use-cases#
  • All-purpose, simple vector container for items.

  • The .append() method is very convienient.

  • You can also .pop(index) an item, and .count(x) the occurrences of x in the list.

Tuples#

Very similar to lists, but are immutable.

a_tuple = (1, 2, 3, "a")  # heterogeneous, generated using parenthesis
a_tuple[2]
3
a_tuple[2] = 4  # immutable!
# So .append() doesn't work, for example. Once created they will remain constant.
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[56], line 1
----> 1 a_tuple[2] = 4  # immutable!
      2 # So .append() doesn't work, for example. Once created they will remain constant.

TypeError: 'tuple' object does not support item assignment
Tuple use-cases#
  • Immutable lists.

  • Return values from functions.

  • Assortments that have a logical connection between them, like coordinates.

Dictionaries#

Python’s hash maps (containers.Map in MATLAB)

  • Structure is key: value.

  • Super useful, O(1) look-ups and assignments.

dictionary = dict(one=2)
dictionary  # curly brackets
# The key is 'one', and its corresponding value is 2
{'one': 2}
another = {'one': 2,
           'two': 3,
           1: 4}
another
{'one': 2, 'two': 3, 1: 4}
print(another['one'])  
# Lookups with brackets, like slicing (but you can't slice dictionaries, they're unordered)
# print(another['three'])
another['four'] = [1, 2, 3]
another['four'][2]
2
3
dict3 = {'one': 100,
         'two': 300,
         'one': 300,
         'three': 100}
dict3  # keys are unique! Values aren't
{'one': 300, 'two': 300, 'three': 100}
dict3['three'] = 1000  # mutable!
print(dict3)
{'one': 300, 'two': 300, 'three': 1000}
# Dictionaries are great for storing nested data - here's some simulated data:
data = {
    'wt': {
        '113': {'mean': 45.3, 'stdev': 3.2}, 
        '545': {'mean': 44.1, 'stdev': 1.2}
    }, 
    'fmr': {
        '674': {'mean': 31.3, 'stdev': 4.4}, 
        '751': {'mean': 36.0, 'stdev': 5.7}
    }
}

print(data['fmr']['674']['stdev'])
4.4

Dictionaries have useful methods, such as .pop(), .keys() and more. We’ll explore them throughout the semester.

Sets#

A set is an unordered mutable data container which forces its items to be immutable and unique.

# Different constructors
set1 = set(['a', 'c', 'e'])
print(set1)

set2 = {'b', 'd', 'f'}
print(set2[0])
# Unordered!
{'a', 'e', 'c'}
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[63], line 6
      3 print(set1)
      5 set2 = {'b', 'd', 'f'}
----> 6 print(set2[0])
      7 # Unordered!

TypeError: 'set' object is not subscriptable

Uniqueness is enforced:

set3 = {'a', 1, 'a', 2}
print(set3)
{1, 2, 'a'}

Can you guess what’s happening here?

set4 = {'a', 1, True, 0, False}
print(set4)
{0, 1, 'a'}
set5 = {1, 2}
set5.add(3)
print(set5)

# Mutable types can't be inserted to a set
set5.add([10, 20])
{1, 2, 3}
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[66], line 6
      3 print(set5)
      5 # Mutable types can't be inserted to a set
----> 6 set5.add([10, 20])

TypeError: unhashable type: 'list'

Sets support set operations like union, intersection and difference:

a_set = {1, 2, 3, 4}
b_set = set([3, 4, 5, 6])
diff_set = a_set - b_set  # difference
print(str(a_set), '-', str(b_set), '=', diff_set)
{1, 2, 3, 4} - {3, 4, 5, 6} = {1, 2}
union_set = a_set.union(b_set)
print(str(a_set), '\u222A', str(b_set), '=', union_set)
{1, 2, 3, 4} ∪ {3, 4, 5, 6} = {1, 2, 3, 4, 5, 6}

Set use-cases#

Sets are less popular, and are mainly used to drop duplicates from lists. Set operations like intersection can also come in handy in some occasions.

list_with_dups = [1, 2, 3, 4, 4, 5]
list_without_dups = list(set(list_with_dups))

Flow Control: if and for#

Exercises#

Below are a couple of exercises that will introduce to you Python’s if statements and the for loop. Their solution is right below.

To get a working Python session quickly, go to repl.it, click on the blue “new repl” button on the top-left part of the screen, choose Python and click “Create Repl”, which should direct you to a screen as seen below. You don’t need to sign up!

Repl.it UI

You can type your code directly on the right side of the screen, or write some code (with functions!) on the left side and click the green “run” button.

Exercise 1: The Enigma#

The Enigma code worked by subtituting each letter with a different one. Write an encoder function which receives the letters to encode and the pairs of letters that are used for the code. The function’s signature should look something like:

def enigma_code(letters, table):
    """Encode\decode the letters using the table. 
    
    If a letter in letters isn't found in table,
    None is returned.
    
    Parameters
    ----------
    letters : iterable
        The letters to encode or decode
    table : dictionary
        A real "dictionary" which defines the translation 
        from one letter to the other
        
    Returns
    -------
    result : list
        A list of translated letters
    """
    # ...
    return result

Exercise 2: List Slicer#

Write a function that slices a list in two parts and returns them. If the list contains an odd number of elements the first slice of the two should contain more items.