Anaconda with Python3 it is! (And Spyder to go with it!)

This post has been last edited on april 28, 2014 to reflect changes in the behavior of both Spyder and Conda.

Scroll a bit down if you’re not interested in the story and just want to know how to install Spyder with Anaconda and Python 3.x :)

Today I decided to switch from Python 2.7 to Python 3.x. I had been thinking about this lately, because at some point the scientific community will have to take the patience to migrate to the backwards-incompatible Python 3, right? Or perhaps to Python 4, given the rate this migration seems to be going.

For me, the date is today, after I noticed the very peculiar way Python 2.7 deals with greater than / less than comparisons.
Long story short, I was writing a simple script using max() when I forgot to use len() on a string:

offset = max(len(enzyme), len(spl1), "Start site")

No error was raised right there, but when I tried to use the result as an int, I got a TypeError – the string was the maximum “value” there. That was so weird… having a method deal with strings and numbers together, with some assumption on their ordering? I needed to understand this awkward behavior somehow better.
Well it turned out that in Python 2.x, using max() on two numbers and a string is considered a thing people will do on purpose, and really raises no error. You can even do "string" > 1000000 and it’ll be ok – the string ends up being greater than the number, because -no kidding- Python 2.7 compares two different types by the name of the type – so “str” is bigger than “int” because “s” > “i”. This apparently results from an interest in having a simple way to order a list of different types, so that say, all the ints would be first, then all strings. I thought that this was very very strange and would bring a lot of confusion, and sure enough, one of the changes in Python 3 is that,

“The ordering comparison operators (<, <=, >=, >) raise a TypeError exception when the operands don’t have a meaningful natural ordering.”

Well then, this was somehow the tipping point for me. Python 3 is logic, Python 2 not equally so: I’m going for Python 3.

I’ve been enjoying Continuum’s Anaconda Python for a while now, so I decided to reinstall Anaconda with Python 3.3, and set up Spyder to work with it. This took a little tweaking, so here’s a step-by-step on how I did it on my system (Windows 7, 64-bit).

Installing Anaconda Python 3.3 and Spyder 2.3 on 64-bit Win 7: step-by-step

1) according to this post from, I downloaded miniconda3.
I’m mostly using 64-bit Windows 7 these days, so the right file for my system was Miniconda3-3.0.5-Windows-x86_64.exe. I installed it for all users, choosing not to set it as default Python 3.3 installation, because I have a standard Python3.3 on the system too. But if you don’t have any other Python3, then by all means set it as default, because some automatic installers (i.e. the psutil one) will need to find Python 3 on the registry.

2) I followed the post; opened a cmd and used conda install anaconda. So at this point I had Anaconda Python 3.3.4 ready at C:\Miniconda3. No Spyder though – only the command-line python executable.

3) Now, off to download the latest version of Spyder. Python3 is stably supported since version 2.3, so choose your version wisely (the last one is spyder-2.3.0beta4 at the time I’m writing).

4) I extracted the spyder-2.3.0beta4 directory in C:\.

5) Now it’s time to build and install it. Open a cmd session. From the command line use the cd command to go to the Spyder extract folder, i.e. in my case I typed:

cd C:\spyder-2.3.0beta4

Now that you are in the right directory, you need to build and install Spyder, and just to stress it again, to do it correctly you must call python exactly from this folder that contains the Spyder files. Otherwise it won’t work.

What you must do is:

C:\Miniconda3\python.exe build
(This will create the “build” subfolder and its files); and then:

C:\Miniconda3\python.exe install
which will install Spyder on Anaconda. Almost done!

Now if you open the Scripts subfolder in your Miniconda folder, you should find there a spyder.bat batch file. You can double-click it to execute Spyder. You can create a link to it and leave the link on your desktop if you like.

You can now work on Spyder’s optional dependencies. To check what you’re missing, open Spyder and go to Help > Optional dependencies. Ideally they should all be “(OK)”, and since in my case several were missing I installed them using conda or pip.
Just a warning: at the time of this writing, if you want to install rope, you should better install it with pip, not conda:

pip install rope_py3k

because (as of today) if you use conda on Python 3, it will download the wrong rope package (apparently) and Spyder will crash on loading.

On the other side, conda was a good way to install matplotlib, pep8, pylint which in my case were not installed together with Anaconda by default. If I remember well I had to use pip to install a few of these the last time (conda was reporting a conflict with Python 3), but now apparently conda managed it ok.

For psutil, since we’re on windows, we get to have an installer, but the downside of it is that it will look for the “official” version of Python 3 in the system registry. If you have set Anaconda Python 3 as your default Python 3 at installation, then you’re good to go. In my case it wasn’t Anaconda, but a regular Python 3.3 installation so I just let the installer complete, then I went to my default Python installation folder, then Lib, site-packages, and I copied the psutil folder plus 3 files: _psutil_mswindows.pyd, _psutil_windows.pyd and psutil-1.2.1-py3.3.egg-info for good measure. (There’s a README.txt there also).
Paste the whole directory, plus the files, in the corrisponding Lib\site-packages subfolder that you will find in the Miniconda3 installation directory. And done!
Enjoy Spyder with Anaconda Python 3.x :)

List comprehensions!


I wanted to write this post since some time. List comprehensions are one of the coolest things I found on Python yet. I’ll probably be adding up things as I go, since there are so many ways you can use them!

Basically a list comprehension is a one-line generator expression used to create a list (or dictionary), starting from one or more pre-existent lists. Since it’s a generator, it’s often less memory-intensive than other methods to build a list, and is often the quickest.

The simplest, bare-bone structure of a list comprehension is:

newlist = [expression_with_element for element in old_list]

i.e. the example below will create a new list where every element of the old list is increased by one:

previous_ages = [23, 34, 37]
new_ages = [age + 1 for age in previous_ages]

Note that the list comprehension is enclosed in brackets [ ].
If you print new_ages you get:

[24, 35, 38]

The basic elements are:
expression_with_element is the operation you want to perform on the items in the old list, to add them to the new list (i.e. in my previous example, I wanted all the elements to be increased by one). This can be an arithmetic operation, a call to a function, combinations of the two, and so on – anything that will return an object to be added to the new list.

for element in old_list specifies which list you’re going to use as starting point (old_list) and what you’re going to call its elements (can be element, i, whatever).
* Now let’s spice things up. Let’s say that I’d like to pick two variables from my old list.
Can I do it? Sure!

previous_ages = [["Mark", 23], ["Jennifer and Carl", 34],
                 ["Katherine", 37]]
new_ages = [[who, age + 1] for who, age in previous_ages]

Here new_ages is:

[['Mark', 24], ['Jennifer and Carl', 35],
 ['Katherine', 38]]

* And what if I want to use if to pick only certain elements from the old list, excluding others? Why, but of course!

previous_ages = [["Mark", 23], ["Jennifer and Carl", 34],
                 ["Katherine", 37]]
new_ages = [[who, age + 1] for who, age in previous_ages
             if age < 37]

Now new_ages is only:

[['Mark', 24], ['Jennifer and Carl', 35]]

* Does this accept and / or too? Of course it does!

previous_ages = [["Mark", 23], ["Jennifer and Carl", 34],
                 ["Katherine", 37]]
new_ages = [[who, age + 1] for who, age in previous_ages
             if age < 37 and who != "Mark"]

If you print new_ages this time:

[['Jennifer and Carl', 35]]

* But this use of if as a predicate is only useful to exclude elements. What if I want to use an if / else to apply different expressions on each item? Why, sure you can! But note that for this the if statement must go before the for loop. This is important!

previous_ages = [["Mark", 23], ["Jennifer and Carl", 34],
                 ["Katherine", 37]]
new_ages = [[who, age + 1] if age < 37 else [who, "Secret!"]
             for who, age in previous_ages]

Katherine won’t reveal her age now:

[['Mark', 24], ['Jennifer and Carl', 35],
 ['Katherine', 'Secret!']]

* So far so good… and what if I have two old lists that I need to use to make the new list, will it accept nested for loops? Watch:

people = ["Dan", "Adam", "Mary"]
adjectives = ["good", "happy", "thoughtful"]
sentences = [person + " is " + adj
             for person in people for adj in adjectives]

Here if you print sentences you get:

['Dan is good', 'Dan is happy', 'Dan is thoughtful',
 'Adam is good', 'Adam is happy', 'Adam is thoughtful',
 'Mary is good', 'Mary is happy', 'Mary is thoughtful']

* Finally for one very peculiar showoff, you can use the peculiar short-circuit behavior of or and and to execute another statement from within the list comprehension (I had posted this little script before, in my post about short circuit evaluations):

bad_fruits = []
a_dict = {'mango': True, 'door': False, 'kiwi': True, 'tree': False}
good_fruits = [fruit for fruit in a_dict.keys()
               if a_dict[fruit] or bad_fruits.append(fruit)]
>>> print(good_fruits)
['kiwi', 'mango']
>>> print(bad_fruits)
['tree', 'door']

That’s all for now! :)

Resources (post in progress)

Educational websites

Receive help on your own questions, but more importantly, nothing beats trying to answer somebody’s question and then seeing how expert programmers would solve it.
Visit it often! And check the “Frequent” tab too!!!

***** Codecademy
Very nice site with lots of coding tutorials.

****  Learn Python the Hard Way
“This simple book is meant to get you started in programming. […]  it uses a technique called instruction. Instruction is where I tell you to do a sequence of controlled exercises designed to build a skill through repetition. This technique works very well with beginners who know nothing and need to acquire basic skills before they can understand more complex topics.”

*** Codility
Codility is a site offering programming tests designed to select job candidates. But it also has a training section which seems interesting.

“A platform for learning bioinformatics through problem solving”
A really good site. Once you solve a problem, you get to see other solutions – a perfect way to learn bioinformatics with Python.

Reference materials

***** Python Docs
‘Nuff said!

***** Tutorials Point: Python
Lots of topics, very well explained.

****  Python Programming @ Wikibooks
Lots of very handy tables and reference lists.

Open books and e-books

***** How to Think Like a Computer Scientist
Very nice, and in a very easy language. An excellent primer!

**** A Byte of Python
Also a nice book! You’ll need to scroll all the way down to find the link to the pdf.

**** Python for Dummies
If it’s on the net, Google has it listed (for all the rest, there’s Torrents).

Other resources

**** Regular Expressions 101
Has a very good regular expression checker, which explains the RE behavior in detail.

It’s all a matter of priorities

So last night a seemingly mild question appeared on Stackoverflow.  A user was asking how to merge a tuple (0, 0) into a tuple of tuples ((1,2), (3, 4)).  Users Amadan and aIKid suggested to use:

x = ((1, 2), (3, 4))
x += ((0, 0),)

and I replied, well, you can also do

x += (0, 0),  # without the outer parentheses

This caused some confusion because some people reported it wouldn’t work, and others confirmed that it worked. What was actually happening was that the statement exactly as I typed it

>>> x += (0, 0),
>>> x
((1, 2), (3, 4), (0, 0))

was working, but the seemingly equivalent

>>> x = x + (0, 0),
>>> x
(((1, 2), (3, 4), 0, 0),)

was not!!

Amadan quickly found out the culprit: the difference between the two statements is in the different priorities of the assignment operator +=, the sum operator + and the comma , .

The assignment operator += has low priority, so the interpreter will first parse the loose comma in (0, 0), and interpret the tuple as ((0, 0),), and only then it will consider the += assignment and merge this tuple to the x tuple. This yields the correct result
((1, 2), (3, 4), (0, 0)).

The plus operator +, on the other side, has a higher priority than the comma , (otherwise you couldn’t correctly parse (1+2, 3+4) without adding more parentheses), so if you try
x = x + (0, 0), the interpreter will first join x and (0, 0) to give the intermediate result
((1, 2), (3, 4), 0, 0), (note the loose comma still there). Then, and only then, it will parse that final loose comma, closing everything in outer parentheses!
(((1, 2), (3, 4), 0, 0),) And you’re screwed.

And to think that when I read about priorities, it was the usual “parentheses, multiplication & division, addition & subtraction” stuff and I just passed thinking “yeah yeah, I know this by now”. There I go, being humbled by a comma :) Serves me right!

Short circuit evaluations in Python


Image from here! (Which is a cool site)

An operator (or a function) is said to short-circuit when it will stop evaluating as soon as it reaches a definitive result. This is obvious for the OR operator, that only needs one of its operands to be True to return True. So in the code

1+1 == 2 or 3**3 == 27

the interpreter will find that “1+1 == 2” evaluates to True, so the OR must return True – what’s the point of evaluating all the rest? In other languages you can force one behavior or the other, but in Python, OR will immediately stop the evaluation when the first operand is True. This behavior is called short circuit evaluation.
The return from the evaluation is so immediate that if the second operand is a call to a function I didn’t even write yet:

1+1 == 2 or ill_write_it_tomorrow()

in Python 2.7 I’m still getting True instead of a “name is not defined” error! (The interpreter will still catch syntax errors though).

What operators or standard functions behave like this in Python?
1) the operator OR (if the first operand is True, it short-circuits to True)
2) the operator AND (if the first operand is False, it short-circuits to False)
3) the function all() (as soon as it finds a False element, it will return False)
4) the function any() (as soon as it finds a True element, it will return True)

Now the fun part! If you look at the official Python docs about OR and AND,

Operation Result
x or y if x is false, then y, else x
x and y if x is false, then x, else y

did you notice those “then y” and “else y”? Do you think what I think? :) The “then y” or “else y” might actually refer to executing a function when x isn’t good enough to short-circuit the evaluation… who’s to stop you? Not the interpreter!

Now, to avoid blowing up the evaluation, also keep in mind that:

not None == True    which means that     not (any function that returns None) == True

Also although None and False are two different things, in IF statements None is considered False, and this helps us in our naughty plans.

For example, here I have a dictionary of names with value True for fruits and False for other items. I want to split the dictionary’s keys into two lists, one for fruits and another for not-fruits. Look (the print notation in this code should work both in Python 2.x and in Python 3.x):

bad_fruits = []
a_dict = {'mango': True, 'door': False, 'kiwi': True, 'tree': False}
good_fruits = [fruit for fruit in a_dict.keys()
               if a_dict[fruit] or bad_fruits.append(fruit)]

The trick is all in the list comprehension at line 3. For each “fruit”,  I add it to good_fruits with the condition:

if a_dict[fruit] or bad_fruits.append(fruit)

When the value of fruit in a_dict is True, then OR short-circuits, the key is added to the new list and the iteration passes to the next item. The second operand is not evaluated. But when a_dict[fruit] is False, then the interpreter evaluates the second operand,


This operand is actually a function that will add the key to the bad_fruits list. It will be executed only when a_dict[fruit] is False, so it will only add bad fruits. It will return None, so the IF condition will not be satisfied and the iteration will pass to the next item. At the end of the iteration, you will get the keys sorted into two nice lists.

A very elegant example – although a bit more complicated – is here in Stackoverflow, with some more explanations here.

Bindings, shallow copy, deep copy

A lesson learned today. From the copy module,

“Assignment statements in Python do not copy objects, they create bindings between a target and an object.”

This is something I always need to keep in mind, because it’s very counter-intuitive for me.

1. Bindings

In Python, if I do

a = [1, 2, 3]

I am binding the name a to a list containing the integers 1, 2 and 3.  But a isn’t the list: a merely refers to the list, it’s just a name I gave to the object [1, 2, 3].

In turn, this means that if I do

a = [1, 2, 3]
b = a

the line b = a is not creating a new list at all: it just means “See what a is pointing to right now? Make b point to that too” – that very same list object. And this is why it’s counter-intuitive for me: I see lots of usefulness in creating a new object b that is a copy of a, and less usefulness in just calling the same thing with two names. Luckily, the more I learn and code, the more it feels natural to get into this frame of mind.

This is crucial information, because if I do:

b[0] = 100

I’m actually modifying an item of the single list object that both a and b are pointing to. If I was planning to use b as the name of a modifiable copy list while a would point to the original values, well, I’d totally be on the wrong track: a and b go hand-in-hand here.

2. Shallow copy

So, this was old news, and I was kind of used to make copies of lists in this way:

a = [1, 2, 3]
b = list(a)

Where line 2 means, “make a new list from all the items contained in the list a points to, and call this new list b“. This was ok for lists where “all the items” meant simple immutable objects, like numbers. In this case b would refer to a new list with the same numbers but no interaction with a, and I could modify it without affecting the original. Perfect.

It was all fun and games until I tried this approach with something more complex – in my case it was a list within another list.

If I write:

a = [1, 2, [10, 20, 30]]
b = list(a)

Now in the first line, a refers to a list object that contains two integers and an inner list. Let me stress this out: the first line not only creates the list object referred to by a, it also creates the inner list referred to by a[2] as an object in its own right. When I write that b = list(a), again b is a new list, a genuinely different object from the one referred to by a. I’m not wrong in this. And as above, b contains all the items that are in the original object. But now the third item is a list object, and since it is an element of the original list, the new list will refer to it, simple as that. The inner list is one and the same for a[2] and b[2]! So if I do:

b[2][0] = 300

I’m modifying the [10, 20, 30] object that is referred to both in a[2] and b[2], so both of them are going to point to the same modified list [300, 20, 30]!
So one can say that in b = list(a), b is a “shallow copy” of a: it points to a new object – a copy of the list referred to by a – but all the objects within it are not copied, they are the same objects of the original list. It’s just that you don’t notice this with immutable types like numbers, because you literally can’t alter the number itself – you can only make the variable point to a different number instead. But if the list contains an object you can alter (i.e. an inner list, a set), and you do alter it, anything that contains that object will reflect the change!
Python tends to not make copies of objects unless specifically requested,  so most ways to copy (like the above, and slicing) result in shallow copies. So what if you want to create b as a brand new object with all brand new objects inside?

3. Deep copy

This is called a “deep copy”,  and a good option to do it is to use the deepcopy method from the copy module. This will create a new object, with all new objects inside, that are identical to the original but completely separate. You can alter whatever you want by b, and the object a refers to will just stay the same. This of course takes cpu time and memory, so do it only when you really have to!

For some more info and more examples (with pictures!): see here.

A Post Scriptum

A similar reasoning applies to local variables. If you pass a variable as argument to a function, a local variable is created that points to the same object. If the function alters this local variable by making it point to another object, the changes will be discarded after the function is over and the local variables are deleted (see the scope of a variable, and namespaces).  But when a line inside the function alters somehow the object the variable is pointing to, i.e. like this:

def testchange(var):
    var[0] = "changed"
a = [1, 2, 3]
print a

then the object will stay changed. So be wary!

Simple fun with strings


Since we can conveniently represent nucleotides and aminoacids as letters, it is no surprise that string methods are awesome ways to get things done in bioinformatics. I’m posting a few very simple things I really like.

1. Slicing

In Python, strings can be sliced and treated as if characters were elements of a list, like this:

my_substring = str[from:to:step]

The default value of “from” is the start of the string; that of “to” is up to and including the last character; and that of “step” is 1 (one character at a time; more on stepping and sliding here).  So if I wanted to invert a string, i could just say “from start to end, at a step of -1”, like this:

str = 'ACAAG'
reverse = str[::-1]
print reverse

This will output: GAACA

2. Uppercase / Lowercase

When checking for identities, I don’t want to have to evaluate for both uppercase and lowercase characters.  So unless the case has some specific meaning, it’s simpler to make a DNA or RNA string all uppercase as soon as I get it:

str = 'acAAg'
str = str.upper()
print str

This will output: ACAAG

3. Taking away what I don’t need with Regular Expressions (the re module)

Sometimes a string that should contain only nucleotides contains something else – spaces, end of line characters, numbers. To remove all of them, leaving only “good” letters, I use the awesome re module, that unleashes the strength of Regular Expressions.
There’s a syntax to learn (and it’s worth it), but a very simple way to use regular expressions to remove unwanted characters is:

import re
str = 'ACC xx wohoo CTGAAbXyTGAGG'
str = re.sub("[^ACTG]", "", str)
print str

This will output: ACCCTGAATGAGG

The highlighted line means:

re.sub(         Use the substitute function from re
"[              Set of characters to match, enclosed in brackets
  ^             this means NOT
   ACTG             uppercase A, or C, or T, or G
       ]",      End of the pattern we're searching
"",             Substitute all these with NOTHING (delete)
str)            do this on the string named str

So the line means “for every character in str that is not A, C, T or G, substitute it with nothing.”

For more info on regular expressions, check the Regular Expression Howto and the re module entry in the Python Standard Library.

4. Using the translate function (from the string module)

re.sub would seem like a smart way to make a reverse complement, but there’s a cooler one!  Can you believe it?  Python has a translate function that takes a minute to code and can do the job almost instantly.
To use it, first you need to create a translation table via a “string.maketrans” function, and then you’ll apply the table to your string, like this:

import string
str = 'ACCTGAGG'
trantab = string.maketrans("ATCG", "TAGC")
complement = str.translate(trantab)
print str

The output is: TGGACTCC

And it’s obvious how the translate function, plus some good slicing, can make a reverse complement pretty quickly.

Installing Python on Windows


Since Microsoft operating systems don’t come with a default installation of Python (Linux distributions do), the first thing to do if you want to tinker with Python is to get a good installation work done.

So, let’s see some options:

1. Installing Spyder

One easy way to get Python 2.7 with all the scientific bell and whistles is to download and install Spyder (which is available for Windows, MacOS, and Linux).

Spyder is a very powerful environment for Python.  It looks quite a bit like Rstudio for the R statistical language.  I like it because it includes, among other features:
– identification of possible coding errors on the fly (ha!), signaled right on the side of the code;
– a “help” window which will automatically load the help info for a function as soon as you write it in your code;
– automatic completion of functions, not only the ones you get by packages, but also the ones you wrote yourself;
– very comfortable text finding, i.e. if you select a text, the location of every instance of the text will be not only highlighted but also shown right on the scroll bar.

Only drawback: the code runs a little more slowly than using the simpler, and much less resource-demanding, Idle interface.  It also runs Python 2.7 by default – not everything might be supported in Python 3.

2. Anaconda Python

If you fancy more bells and more whistles, you might want to download Anaconda Python. It is a very complete distribution of Python, and it includes Spyder.
What’s more, if you happen to work in an educational institution, you might qualify for a free download of the Accelerate add-on, which will harness not only the full power of your computer’s multi-coreness, but even the computational abilities of your graphic card, to Python’s use.

3. Using basic Python and Idle

If on the other side you prefer a leaner installation, then just download the basic Python files.  Not all modules work with Python 3, so you might as well install the 32-bit 2.7 version too. Then, scientific modules!  NumPy, SciPy and matplotlib looked like a good start.  In order to use matplotlib, you need a few basic modules that don’t come with base Python (“dependencies”), so I listed those too, below.
Be sure to download the file corresponding to the version of Python you have! Specifically, if your machine is 64-bit but you have Python 2.7 32-bit installed, then download the 32-bit version of all files.
So here’s the whole list of modules I installed on my system:
…and finally, if you want,

And there you go!  PYTHON!

Oh, a final note: the Scipy-stack meta-package contains NumPy, SciPy, matplotlib, and more.  It’s all in one file to download – but the version for Python 2.7, win32 is 110 MB.

My first steps with Python (and doing bioinformatics with it)


I have entered the wonderful world of Python via a MOOC called “An Introduction to Interactive Programming in Python“, on the Coursera website.  It took quite a bit of my sleeping time, but I did it well, and it paid off.  If you’re new to Python and need to start somewhere, that course is a nice start.

– You’re coding games, so it’s really fun.
– Being a MOOC, you have access to a discussion forum visited by thousands of other students.
– You’re using a simpler interface for Python, called CodeSkulptor.  And yes, you can migrate to “standard” Python quite easily afterwards.

– Bye bye, sleep.  The course really took a lot of my spare time.
– You get a good idea of Python, but if you weren’t a coder before, you’re still not a coder by far (I know I’m not).  There is a whole lot of very basic info on how to program that you just don’t get with a short course, and it’s kind of the foundation of good coding.  I’m also learning these foundations “on the fly”, hoping I won’t carry on too many bad habits. :)

Right now I’m starting another course, called Bioinformatics Algorithms (Part 1).
It is NOT a programming course.  It’s a course about algorithms – you can code them with whatever language you like.  Again, it’s pretty heavy on my time, but I’m really enjoying it so far. It is linked to two other very good educational sites:

Rosalind – an ordered collection of bioinformatics problems you can solve.  Created by some of the MOOC’s instructors. Very good, and I must say that the name is by itself awesome since it comes from Rosalind Franklin, the woman whose findings on X-ray crystallography of DNA contributed significantly to Watson and Crick discovering the structure of DNA (Rosalind’s logo is a simplified version of the X ray diffraction of DNA).
W&C didn’t acknowledge her contribution much.  Science was annoyingly sexist at the time.  It still is today! Imagine how it was in the ’50s.  Anyways.  Check the Rosalind website!  And once you’ve solved a problem, don’t forget to check the solutions uploaded by others – some are absolutely beautiful in their simplicity! (You’ll hate yourself.)

Stepic – an educational site the instructors are promoting via the MOOC.  Interesting too, and quite broad (there are other courses there, including more material on Python).

So… here I am.  I learn “on the fly” because I’m either trying to do some script to help me at work, or I’m trying to solve exercises for a MOOC, and I try to do it as best as possible, picking up information as I go.  And writing it here.  Feel free to visit!