Python is an easy language to learn. And there are many self-taught programmers who don't really go with the best practices from the start. Very often during the development process and when viewing the solutions of our users in CheckiO we are faced with a lot of these mistakes. For this reason in this article I highlighted the most common beginner mistakes, so nobody repeat them again.

Incorrect indentation, tabs and spaces

Never use tabs, only spaces. Use 4 spaces for a tab. And follow a consistent indentation pattern, because many Python features rely on indentation. If your code is executing a task when it shouldn't then review the indentation you’re using.

Using a Mutable Value as a Default Value

def foo(numbers=[]): numbers.append(9) return numbers

We take a list, add 9.

>>> foo() [9] >>> foo(numbers=[1,2]) [1, 2, 9] >>> foo(numbers=[1,2,3]) [1, 2, 3, 9]

And here what happens when calling foo without numbers.

>>> foo() # first time, like before [9] >>> foo() # second time [9, 9] >>> foo() # third time... [9, 9, 9] >>> foo() # WHAT IS THIS BLACK MAGIC?! [9, 9, 9, 9]

In Python default values or functions are instantiated not when the function is called, but when it's defined.

Solution

def foo(numbers=None): if numbers is None: numbers = [] numbers.append(9) return numbers

Well, some other default values do work as expected.

def foo(count=0): count += 1 return count

>>> foo() 1 >>> foo() 1 >>> foo(2) 3 >>> foo(3) 4 >>> foo() 1

The reason for this is not in the default value assignment, but in the value itself. An integer is an immutable type. Doing count += 1 the original value of count isn’t changing.

Try to call a function as the default value:

def get_now(now=time.time()): return now

As you can see, while the value of time.time() is immutable, it returns the same time.

>>> get_now() 1373121487.91 >>> get_now() 1373121487.91 >>> get_now() 1373121487.91

Handle specific exceptions

If you know what exception the code is going to throw, just except that specific exception.

Don't write like this:

try: # do something here except Exception as e: # handle exception

When on a Django's model you're calling get() method and the object is not present, Django throws ObjectDoesNotExists exception. You need to catch this exception, not Exeption .

Write a lot of comments and docstrings

As we've already talked about in one of our articles, if you can't make the code easier and there is something not so obvious, you need to write a comment.

Writing docstrings to functions/methods is also a good habit.

def create_user(name, height=None, weight=None): '''Create a user entry in database. Returns database object created for user.''' # Logic to create entry into db and return db object

Scoping

Python understands global variables if we access them within a function:

bar = 42 def foo(): return bar

Here we're using a global variable called bar inside foo and it works as it should:

>>> foo() 42

It also works if we use some function on a global

bar = [42] def foo(): bar.append(0) >>> bar [42, 0]

What if we change bar ?

>>> bar = 42 ... def foo(): ... bar = 0 ... foo() ... return bar 42

Here the line bar = 0 , instead of changing bar , created a new, local variable also called bar and set its value to 0 .

Let's look at a less common version of this mistake to understand when and how Python decided to treat variables as global or local. We'll add an assignment to bar after we print it:

bar = 42 def foo(): print(bar) bar = 0

This shouldn't break our code. But it does.

>>> foo() Traceback (most recent call last): File " ", line 1, in foo() File " ", line 3, in foo print bar UnboundLocalError: local variable ''bar'' referenced before assignment

There are two parts to this misunderstanding:

1) Python is being executed statement-by-statement, and NOT line-by-line,

2) Python statically gathers informations about the local scope of the function when the def statement is executed. Reaching bar=0 it adds bar to the list of local variable for foo .

bar = 42 def foo(baz): if baz > 0: return bar bar = 0

Python can't know that the local bar was assigned to.

bar = 42 def foo(): return bar if False: bar = 0

Running foo we get:

Traceback (most recent call last): File " ", line 1, in foo() File " ", line 3, in foo return bar UnboundLocalError: local variable 'bar' referenced before assignment

Python still declares bar as statically local.

Solutions:

1. Using the global keyword, but it is very often a bad idea, if you can avoid using global it is better to do so.

>>> bar = 42 ... def foo(): ... global bar ... print(bar) ... bar = 0 ... ... foo() 42 >>> bar 0

2. Don't use a global that isn't constant. If you want to keep a value that is used throughout your code, define it as a class attribute for a new class.

>>> class Baz(object): ... bar = 42 ... ... def foo(): ... print(Baz.bar) # global ... bar = 0 # local ... Baz.bar = 8 # global ... print(bar_ ... ... foo() ... print(Baz.bar) 42 0 8

"How To Use Variables in Python 3" - a very detailed explanation by Lisa Tagliaferri

Edge cases first

Pseudo code 1:

if request is post: if parameter 'param1' is specified: if the user can perform this action: # Execute core business logic here else: return saying 'user cannot perform this action' else: return saying 'parameter param1 is mandatory' else: return saying 'any other method apart from POST method is disallowed'

Pseudo code 2:

if request is not post: return saying 'any other method apart from POST method is disallowed' if parameter 'param1' not is specified: return saying 'parameter param1 is mandatory' if the user cannot perform this action: return saying 'user cannot perform this action' # Execute core business logic here

These pseudo codes are for handling a request, and the second one is much easier to read, it doesn't have a lot of nesting, which avoids common problems.

Copying

>>> a = [2, 4, 8] >>> b = a >>> a[1] = 10 >>> b [2, 10, 8]

As you can see a and b are pointing at the same object, and thus by changing a - b will also change.

You can google this trick.

>>> b = a[:] >>> a[1] = 10 >>> b [2, 4, 8] >>> a [2, 10, 8]

Or the copy method.

>>> b = a.copy() >>> a[1] = 10 >>> b [2, 4, 8] >>> a [2, 10, 8]

In this case we can see that the b away is an a list copy, but by changing a we don't change b.

Although here you can also step on a rake with the nested lists.

>>> a = [[1,2], [8,9]] >>> b = a.copy() >>> a[0][0] = 5 >>> a [[5, 2], [8, 9]] >>> b [[5, 2], [8, 9]] >>>

Or, for example, the multiplication of list.

>>> a = [[1,2]] * 3 >>> a [[1, 2], [1, 2], [1, 2]] >>> a[0][0] = 8 >>> a [[8, 2], [8, 2], [8, 2]]

To avoid it once and for all use deepcopy.

>>> from copy import deepcopy >>> c = deepcopy(a) >>> a[0][0] = 10 >>> a [[10, 2], [8, 9]] >>> b [[10, 2], [8, 9]] >>> c [[8, 2], [8, 9]]

Related comment-thread

Creating count-by-one errors on loops

Remember that a loop doesn't count the last number you specify in a range. So if you specify the range (1, 11), you actually get output for values between 1 and 10.

>>> a = list(range(1, 11)) >>> a [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] >>> a[0] 1 >>> a[0:5] [1, 2, 3, 4, 5]

Wrong capitalization

If you can't access a value you expected, you have to check capitalization. Python is case sensitive, so MyVar is different from myvar and MYVAR.

>>> MyVar = 1 >>> MYVAR Traceback (most recent call last): File "< stdin >", line 1, in < module > NameError: name 'MYVAR' is not defined

Using class variables incorrectly

>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print A.x, B.x, C.x 1 1 1

Example makes sense.

>> B.x = 2 >>> print A.x, B.x, C.x 1 2 1

And again.

>>> A.x = 3 >>> print A.x, B.x, C.x 3 2 3

You ask why did C.x change if we've only changed A.x.? Well, class variables in Python are internally handled as dictionaries and follow the Method Resolution Order (MRO). That's why the attribute x will be looked up in its base classes (only A in the above example, although Python supports multiple inheritance) since it's not found in class C. So, C doesn't have its own x property, independent of A. And like that references to C.x are in fact references to A.x. This causes a Python problem unless it's handled properly.

Conclusion

As you can see, there are a lot of things that could be done wrong, especially if you're new to Python. But by keeping in mind this mistakes you can pretty easily avoid them. You just need a little practice and it'll become a habit. Nevertheless, it's not nearly the whole list and there might be the things you'd want to add. So, what kind of mistakes did you encounter besides those mentioned above?

Related articles