12

I'm wondering specifically what experienced programmers thought when they started developing in Python. I'm sure the answer depends on your background, but my own personal answer is the conversion of basically anything in the language to a True/False value in boolean contexts.

Resulting in "oddities" like:

if x:

not meaning the same thing as:

if x == True:

I understand why, but it bugs me, and I certainly had to think about it a bit when I first ran into it.

12

For me it's the unexpected behavior of scopes in some situations, for example:

>>> functions = [lambda x:x*multiplier for multiplier in range(10)]
>>> [f(10) for f in functions]
[90, 90, 90, 90, 90, 90, 90, 90, 90, 90]

This is not what I would expect at first sight. One possible correct version is as follows:

>>> def makemul(multiplier):
>>>    return lambda x:x*multiplier
>>> functions = [makemul(multiplier) for multiplier in range(10)]
>>> [f(10) for f in functions]
[0, 10, 20, 30, 40, 50, 60, 70, 80, 90]

Here's another:

>>> functions = [(lambda m: lambda x:x*m)(multiplier) for multiplier in range(10)]
>>> [f(10) for f in functions]
[0, 10, 20, 30, 40, 50, 60, 70, 80, 90]

Another fact that may come as a surprise is that default values for function parameters are assigned during the evaluation of the def statement, not during function calls. Here's an example (from "Python in a nutshell"):

def f(x, y=[]):
    y.append(x)
    return y
print f(23)    # prints [23]
print f(42)    # prints [23, 42] (!)

If you want to bind y to an empty list at each function call you must use a workaround:

def f(x, y=None):
    if y is None: y=[]
    y.append(x)
    return y
print f(23)    # prints [23]
print f(42)    # prints [42]
12

Newbies might find the underscore conventions confusing. There are 4 conventions:

  1. __xyz__ => for magic stuff (defined by the language)
  2. __xyz => for private stuff in a class
  3. _xyz => for things you do not want to be imported with "from my_module import *"
  4. xyz_ => for variables whose name clash with a reserved keyword

I often forget an underscore here and there.

Here's an example to show all four conventions:

# module test_module.py
class _Foo:
    def __some_private_method(self):
        print "This is private!"
    def __str__(self): # a magic method (not private)
        return "This is pure magic."
    def register_class(class_): # use "class_" instead of "klass" or "clazz"
        print "Registering class",class_

>>> from test_module import *  # does not add _Foo to the current scope
>>> x = _Foo()  # nope!
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name '_Foo' is not defined
>>> import test_module
>>> x = test_module._Foo() # ok!
>>> from test_module import _Foo # also works
>>> x = _Foo() # yep
>>> x.__some_private_method() # no, it's private!
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: _Foo instance has no attribute '__some_private_method'
>>> x._Foo__some_private_method() # it's actually just name-mangling
This is private!
>>> print x  # this calls magic method __str__
This is pure magic.
>>> x.register_class(dict)
Registering class <type 'dict'>
9

A newbie might complain about having to type self everywhere in instance methods:

class Employee:
    def salary(self):
        return self.__salary
7

As a newbie, I got mixed up with old-style and new-style classes. A lot of times I forgot to extend the object class, and some things did not work as I expected, for example properties don't work in old-style classes:

class Employee:  # forgot to extend object!
    def set_salary(self, salary):
        print "New salary:",salary
        self.__salary=salary
    def get_salary(self):
        print "Current salary:",self.__salary
        return self.__salary
    salary = property(get_salary, set_salary)

>>> john=Employee()
>>> john.salary=40000  # no display!

In this example, the set_salary method was not called. If you make the Employee class extend the object class, you get a new-style class, and everything works fine:

class Employee(object): # ok!
    #...   the rest is the same

>>> john=Employee()
>>> john.salary=50000
New salary: 50000
>>> x = john.salary
Current salary: 50000
6

A newbie might wonder why python needs both tuples and lists (at least I did):

alist  = [2,3,5,7]
atuple = (2,3,5,7)

Tuples are immutable, which allows you to use them as keys in dictionaries, for example:

bands = {}
bands[("John","Lennon")]="Beatles"
bands[["John","Lennon"]]="Beatles" # can't use lists as keys

Plus, the syntax for a tuple containing a single element is awkward:

one_element_tuple = (1,)

I am pretty sure tuples confuse a lot of newbies, and I'm also pretty convinced that they don't add much to the language (I am interested in your point of view, though).

4

For me the meaning of i=[] in the following code was a bit awkward.

class A():
    i = []                   #creates a class attribute
    def m(self):             
       self.i.append(7)      #accesses an object attribute or the class attribute                                 
       self.i = []           #obscures the class attribute with the object attribute

Having the class attributes accesible exactly the same way as object attributes and their syntax being similar to object attribute syntax in other languages led me to think that 'i=[]' meant "Set the i attribute on each object created in this class to a fresh emptly list". Needless to say, this is not the case :)

4

If I started programming python now, I'd think the worst aspect is the lack of well-behaving closures and scope.

def broken(a):
    def f():
        a = a + 5
    f() # error

i = 10
for i in range(100):
    pass
assert i == 10 # breaks, i == 99
3

As much as I love Python, the documentation is poor. It's nearly impossible to go to the docs and find some random function when you have no idea of where it will be or what it'll be called. You're nearly always forced to google it.

3

For me it's the indenting. As long as you are aware of the editors that you use and how they deal with tabs you are all right. If you switch up editors a lot and sometimes use tabs you might get bitten. I've run into this with stuff I have on my usb thumb drive and moving around to different systems using different editors.

Once I ran into it, I made sure the editors I use all expand tabs to spaces.

3

Worst thing for me was the required ':' at the end of certain block-starting statements. It shouldn't be needed when the following line is indented, e.g.:

if test
    do_this
else
    do_that

Well, if that's the worst thing, then I guess I was pretty happy with the language overall. :-)

2

That behaviour isn't really surprising to me, because in many other languages there is the same effect. For example, in C (if x is an integer):

if (x) { ... }
if (x == 1) { ... }

Those are not the same if x is 5. Each language has its own rules regarding what happens if a non-boolean variable is used in a boolean context, and Python's rules are well-defined and (I think) sensible. The rule is (from the Language Reference section 5.10):

In the context of Boolean operations, and also when expressions are used by control flow statements, the following values are interpreted as false: False, None, numeric zero of all types, and empty strings and containers (including strings, tuples, lists, dictionaries, sets and frozensets). All other values are interpreted as true.

Python's rules do mean that the following act differently (suppose a is a list):

if a:             # false if a is empty
if a is not None: # true if a is empty
1

@Federico Ramponi: I beleive the first example works like it does because of how the multiplier variable is scoped. In general, I often find it difficult to figure out the scoping of a variable just by looking at the code. At write time, it is usually clear, although you can get strange, unexpected behavior by accidentally reusing a name at a lower level. With explicit variable declarations, this could be avoided / signaled by complier warnings.

The other feature that was really strange for me at first is the layout based grouping. I really would not like to start a flame war on this issue, it has been discussed in many-many places before, but it CAN get confusing IF you mix spaces and tabs, and use at least two different editors/viewers to browse your code. The former is of course strongly advised against in most python FAQs.

1

I don't find Python's reporting of syntax errors very helpful. This is quite important for newbies since they will hit a lot of these errors when learning Python. I remember getting frustrated by them when I was learning the language.

For example, if you run the following script

x = ([1, 2, 3]    # note the missing parenthesis
y = 8

you get the following response:

  File "C:\Python25\test.py", line 2
    y = 8
    ^
SyntaxError: invalid syntax

This kind of error message will confuse someone learning the language when they try to look for syntax errors in perfectly valid lines of code. Also, invalid syntax doesn't tell them much about what they've done wrong.

1

The worst (also arguably the best) aspect of Python is the fact that any reference/cookbook you may buy is almost antiquated upon the next version release. Not only do many new modules and language constructs get added, allowing for more efficient/readable solutions, but can also get removed (for ex: lambda, map, reduce).

1

One thing I had issues with when starting Python was the whole "import"/"from import" business. There are many different ways to organize your files and directories in Python, and each method requires a different "import" syntax.

After I read through the documentation two or three times, though, I started to catch on.

1

In Python, you can use the multiplication operator to copy string and list contents.

Be careful if the list content is a reference (object), as in Example 3 below. Python copies the reference. If you use the reference to change the contents of one object, you change the contents of the others, too.

I tested the non-comment code below in Python 3.

# Example 1: get 'aaa'

3 * "a"

# Example 2: a is [1, 1], and then [3, 1]

a = 2 * [1]

a[0] = 3

# Example 3: Be careful. a is [[], []], and then [[3], [3]]

a = 2 * [[]]

a[0].append(3)

In Example 3, we get the same results if we have a = 2 * [list()]. To avoid the final result of Example 3, we can use a list comprehension.

# Example 4: a is [[], []], and then [[3], []].

a = [[] for i in range(2)]

a[0].append(3)

0

Greg, I agree that all languages deal with it a bit differently. But if you look at the example in the comments to my original question my point is perhaps stronger.

This:

if x:

is not the same as:

if x != False:

In your C example (and in many other languages) the above lines would be equivalent. But in Python they are not. The first one just converts x to a bool and tests it, the second one negatively equates x with False (which is not the same). Use x = "" and the first line is false and the second is true.

0

The whitespace issues, specifically the sensitivity the tabs versus spaces! (-:

You might like to look at the book "Dive Into Python" which is available for free under a GNU Free Documentation License.

HTH

cheers,

Rob