-
Notifications
You must be signed in to change notification settings - Fork 0
Python Guidelines
Generally, don't edit files just to change the style. But do aim for this style with new or modified code.
See also http://edx-developer-guide.readthedocs.org/en/latest/testing/code-quality.html
Follow PEP 8
- 4-space indents (no tabs)
- Names like this: modules_and_packages, functions_and_methods, local_variables, GLOBALS, CONSTANTS, MultiWordClasses
- Acronyms should count as one word: RobustHtmlParser, not RobustHTMLParser
- Trailing commas are good: they prevent having to edit the last line in a list when adding a new last line. You can use them in lists, dicts, function calls, etc.
- EXCEPT: we aren't (yet) limiting code lines to 79 characters. Use 120 as a limit for code. Please use 79 chars as a limit for docstring lines though, so that the text remains readable.
Follow these guidelines:
- Try to refactor the code to not need such long lines. This is often the best option, and is often overlooked for some reason. More, shorter lines are good.
- If you need to break a function call over more than one line, put a newline after the open paren, and move the arguments to their own line. DO NOT indent everything to where the open paren is. This makes the code too indented, and makes different function calls near each other indented different amounts.
# NO NO NO!:
results = my_object.some_method(arg1, # this is very arg2, # very ugly and makes arg3, # the code squished over on the right. )
# YES:
results = my_object.some_method( arg1, arg2, arg3, )
# OR:
results = my_object.some_method( arg1, arg2, arg3 )
Important points:
- Don't over-indent to make things line up with punctuation on the first line.
- Closing paren should be on a line by itself, indented the same as the first line.
- The first line ends with the open paren.
PEP 8 recommends a most-general to most-specific import order, which means this order:
- Standard library imports
- Third Party Library imports
- Core Django imports
- Third party Django app imports
- Other edX repo imports
- Local imports
Follow PEP 257
- Write docstrings for all modules, classes, and functions.
- Use three double-quotes for all docstrings.
- Start with a one-line summary. If you can't fit a summary in one line, think harder, or refactor the code.
- Write in Sphinx-friendly prose style. Put backquotes around code names (variables, parameters, methods, etc).
The preferred style is so-called "Google Style" with readable headers for different sections, and all arguments and return values defined.
Note: there is one exception. REST APIs created using Django REST Framework (DRF) must use a hybrid format that is suitable both for DRF and ReadTheDocs. This format is documented here: https://openedx.atlassian.net/wiki/display/AC/edX+REST+API+Conventions#edXRESTAPIConventions-docstringsDocstringFormat
See:
- http://sphinxcontrib-napoleon.readthedocs.org/en/latest/example_google.html
- https://google-styleguide.googlecode.com/svn/trunk/pyguide.html?showone=Comments#Comments
Here's how you write documentation in "Google Style":
def func(arg1, arg2): """Summary line. Extended description of function. Arguments: arg1 (int): Description of arg1 arg2 (str): Description of arg2 Returns: bool: Description of return value """
Most of our code is written using an older style:
def calculate_grade(course, student): """ Sum up the grade for a student in a particular course. Navigates the entire course, adding up the student's grades. Note that blah blah blah, and also beware that blah blah blah. `course` is an `EdxCourseThingy`. The student must be registered in the course, or a `NotRegistered` exception will be raised. `student` is an `EdxStudentThingy`. Returns a dict with two keys: `total` is a float, the student's total score, and `outof` is the maximum possible score. """
If you only have a single line in your docstring, first, consider that this is almost certainly not enough documentation, and write some more. But if you do have just one line, format it similarly to a multi-line docstring:
def foo(a, b): """ Computes the foo of a and b. """
Not like this:
def foo(a, b): """Computes the foo of a and b.""" # NO NO NO
Each class should have a __repr__() method defined, so that calling repr() on an instance of the class returns something meaningful that distinguishes objects from each other to a human being. This is useful for debugging purposes.
-
For unused args, you can prefix the arguments with _ to mark them as unused (as convention), and pylint will accept that.
-
Adding a TODO in one place requires you to make a pylint fix in another (just to force us to clean up more code)
-
No bare except clauses.
except:
should beexcept Exception:
, which will prevent it from catching system-exiting exceptions, which we probably shouldn't be doing anyway. If we need to, we can catchBaseException
(There's no point in catchingBaseException
, that includes the exceptions we didn't want to catch withexcept:
in the first place.) (ref: http://docs.python.org/2/library/exceptions.html#bltin-exceptions). CatchingException
, however, will still generate a pylint warning "W0703: catching too general exception." If you still feel that catchingException
is justified, silence the pylint warning with a pragma: "# pylint: disable=broad-except" -
Although we try to be vigilant and resolve all quality violations, some Pylint violations are just too challenging to resolve, so we opt to ignore them via use of a pragma. A pragma tells Pylint to ignore the violation in the given line. An example is:
self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access
The pragma starts with a #
two spaces after the end of the line. We prefer that you use the full name of the error (pylint: disable=unused-argument
as opposed to pylint: disable=W0613
), so it's more clear what you're disabling in the line.
It's better to use a class or a namedtuple
to pass around data that has a fixed shape than to use a dict
. It makes it easier to debug (because there is a fixed, named set of attributes), and it helps prevent accidental errors of either setting new attributes into the dictionary (which might, for instance, get serialized unexpectedly), or might be typos.
Always import from the root of the project:
from lms.djangoapps.hologram.models import 3DExam # GOOD from .models import 3DExam # GOOD from hologram.models import 3DExam # BAD!
The second form (relative import) only works correctly if the importing module is itself imported correctly. As long as there are no instances of the third form, everything should work. Don't forget that there are other places that mention import paths:
url(r'^api/3d/', include('lms.djangoapps.hologram.api_urls')), # GOOD url(r'^api/3d/', include('hologram.api_urls')), # BAD! @patch('lms.djangoapps.hologram.models.Key', new=MockKey) # GOOD @patch('hologram.models.Key', new=MockKey) # BAD!
- Fat Models, Helper Modules, Thin Views, Stupid Templates