A Python teaching tool
Python JavaScript
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.


Choosy: A tool for writing Python exercises

Choosy is an experiment in writing interactive Python learning exercises.
Instructors write exercises as programming problems, and also write code
to check the results of the students' work.

Students complete exercises, then choosy runs their code, then runs the
instructor's check code to determine the outcome of the student's work.

Development server

Choosy is a standard Django project.  To run the development server, use pip
to install the requirements, create a database, and then run the Django 

    $ cd server
    $ pip install -r devrequirements.txt
    $ python manage.py syncdb
    $ python manage.py runserver

NOTE: the development server doesn't use a sandbox for executing student and
teacher code! If you try malicious things, they will execute directly on your
local system.  Don't test the security of the sandbox, or run exercises from
unknown sources on your development server!

Writing exercises

There are two people involved in an exercise: the teacher, who writes the 
exercise, and the student working the exercise.

Exercises consist of:

    * Slug, the short URL-safe name of the exercise.

    * Name, the human-readable name of the exercise.

    * Text, the written description of the problem the student must solve.
      This is written in Markdown.
    * Check: the teacher's Python code for checking the student's work.
      More on this below.

    * Solution: a solution to the exercise.

Check code

The teacher's check code is a Python module with a single function defined:

    check(t, c)

    t is a Trial object, containing the results of running the student's code.

    c is Checker object, with methods for recording expectations and outcomes.

    check() returns nothing, it uses the c object to indicate results.

Trial objects

A Trial object has these attributes:

    module: the module object created by running the student's code. If you
    want to examine the my_list variable created by the student, it is 

    stdout: a string, the data written to stdout by the student's code.

Trial objects have these methods:

    names(): returns the list of names defined by the user in their code.

Checker objects

The teacher uses Checker objects in their check code to indicate expectations
and outcomes.  The check function is written as a series of expect clauses:

    def check(t, c):
        with c.expect("a should equal b."):
            if t.module.a != t.module.b:
                c.fail("Your a and b aren't equal: %r != %r." % (t.module.a, t.module.b))
        with c.expect("foo() should return 17."):
            his_foo = t.module.foo()
            if his_foo != 17:
                c.fail("Your foo() returned %r." % his_foo)

Each c.expect(msg) clause produces an item in the student's results.  If c.fail(msg)
isn't called, then it will be a success result:

    (green) a should equal b.

If c.fail(msg) is called, then it will be a failed result, mentioning both the
expectation and the failed results, for example:

    (red) a should equal b. Your a and b aren't equal: 12 != 'hello'.

As a shorthand, c.test(cond, msg) will call c.fail(msg) if cond is false.
With it, we can shorten our example to:

    def check(t, c):
        with c.expect("a should equal b."):
            a, b = t.module.a, t.module.b
            c.test(a == b, "Your a and b aren't equal: %r != %r." % (a, b))

        with c.expect("foo() should return 17."):
            his_foo = t.module.foo()
            c.test(his_foo == 17, "Your foo() returned %r." % his_foo)

Both c.fail and c.test can be called with no message at all, in which case
the failure result will simply have the expectation mentioned:

    (red) a should equal b.

Once c.fail is called, no further code in the check function is run. This
is usually what you want, since if an early expectation is unmet, it probably
doesn't make much sense to check later ones.  You can change this behavior
if you'd like to continue on from a failed expectation:

    def check(t, c):
        with c.expect("1 should be 2.", continue_on_fail=True):
            c.test(1 == 2, "That's odd, it doesn't.")
        with c.expect("a should equal b."):
            # etc..

For very thorough checks, you may have expectations that almost all students
will pass, and you don't want to clutter up the results with them.  For
example, once students are writing functions, the teacher expects them 
to have a defined a function with the correct name, but if the results
page said, "You should have a function named average," it could sounds
patronizing.  If your expectation has quiet=True, then it will not
appear as a success result, but will appear if it fails:

    def check(t, c):
        with c.expect("You should have a function named average.", quiet=True):
            c.test("average" in t.names(), "You have nothing named average.")
            c.test(callable(t.module.average), "Your average isn't a function.")

        # more expectations, calling average and checking the results...

Checking function results

The Checker object c has a helper method for checking function results:

    def check(t, c):
        c.function_returns(t.module, 'sum', [
            (1, 1,      2),
            (2, 3,      5),
            (10, 99,    109),
            (-3, -6,    -9),

The first argument is an object, usually t.module, that should have the
function.  The second argument is the name of the expected function.
function_returns will examine the object for the named function using
quiet expectations.  The third argument is a list of tuples.  Each
tuple represents one function call.  The last value is the expected return
value when the other values are used as arguments to the function.
Each function call produces one line of results:

    (green) sum(1, 1) should return 2.
    (green) sum(2, 3) should return 5.
    (red) sum(10, 99) should return 109.  You returned 999.
    (green) sum(-3, -6) should return -9.