Note This is about packaging libraries, not applications. ⸻ All the advice here is implemented in a project template (with full support for C extensions): cookiecutter-pylibrary (introduction).

I think the packaging best practices should be revisited, there are lots of good tools now-days that are either unused or underused. It's generally a good thing to re-evaluate best practices all the time.

I assume here that your package is to be tested on multiple Python versions, with different combinations of dependency versions, settings etc.

And few principles that I like to follow when packaging:

If there's a tool that can help with testing use it. Don't waste time building a custom test runner if you can just use py.test or nose. They come with a large ecosystem of plugins that can improve your testing.

When possible, prevent issues early. This is mostly a matter of strictness and exhaustive testing. Design things to prevent common mistakes.

Collect all the coverage data. Record it. Identify regressions.

Test all the possible configurations.

The structure * This is fairly important, everything revolves around this. I prefer this sort of layout: ├─ src │ └─ packagename │ ├─ __init__.py │ └─ ... ├─ tests │ └─ ... └─ setup.py The src directory is a better approach because: You get import parity . The current directory is implicitly included in sys.path ; but not so when installing & importing from site-packages . Users will never have the same current working directory as you do. This constraint has beneficial implications in both testing and packaging: You will be forced to test the installed code (e.g.: by installing in a virtualenv). This will ensure that the deployed code works (it's packaged correctly) - otherwise your tests will fail. Early. Before you can publish a broken distribution. You will be forced to install the distribution. If you ever uploaded a distribution on PyPI with missing modules or broken dependencies it's because you didn't test the installation. Just beeing able to successfuly build the sdist doesn't guarantee it will actually install!

It prevents you from readily importing your code in the setup.py script. This is a bad practice because it will always blow up if importing the main package or module triggers additional imports for dependencies (which may not be available ). Best to not make it possible in the first place.

Simpler packaging code and manifest. It makes manifests very simple to write (e.g.: you package a Django app that has templates or static files). Also, zero fuss for large libraries that have multiple packages. Clear separation of code being packaged and code doing the packaging. Without src writting a MANIFEST.in is tricky . If your manifest is broken your tests will fail. It's much easier with a src directory: just add graft src in MANIFEST.in . Publishing a broken package to PyPI is not fun.

Without src you get messy editable installs (" setup.py develop " or " pip install -e "). Having no separation (no src dir) will force setuptools to put your project's root on sys.path - with all the junk in it (e.g.: setup.py and other test or configuration scripts will unwittingly become importable).

There are better tools. You don't need to deal with installing packages just to run the tests anymore. Just use tox - it will install the package for you automatically, zero fuss, zero friction.

Less chance for user mistakes - they will happen - assume nothing!

Less chance for tools to mixup code with non-code. Another way to put it, flat is better than nested - but not for data. A file-system is just data after all - and cohesive, well normalized data structures are desirable. You'll notice that I don't include the tests in the installed packages. Because: Module discovery tools will trip over your test modules. Strange things usually happen in test module. The help builtin does module discovery. E.g.: >>> help('modules') Please wait a moment while I gather a list of all available modules... __future__ antigravity html select ...

Tests usually require additional dependencies to run, so they aren't useful by their own - you can't run them directly.

Tests are concerned with development, not usage.

It's extremely unlikely that the user of the library will run the tests instead of the library's developer. E.g.: you don't run the tests for Django while testing your apps - Django is already tested. Alternatives * You could use src -less layouts, few examples: Tests in package Tests outside package ├─ packagename │ ├─ __init__.py │ ├─ ... │ └─ tests │ └─ ... └─ setup.py ├─ packagename │ ├─ __init__.py │ └─ ... ├─ tests │ └─ ... └─ setup.py These two layouts became popular because packaging had many problems few years ago, so it wasn't feasible to install the package just to test it. People still recommend them even if it based on old and oudated assumptions. Most projects use them incorectly, as all the test runners except Twisted's trial have incorrect defaults for the current working directory - you're going to test the wrong code if you don't test the installed code. trial does the right thing by changing the working directory to something temporary, but most projects don't use trial .

The setup script * Unfortunately with the current packaging tools, there are many pitfalls. The setup.py script should be as simple as possible: #!/usr/bin/env python # -*- encoding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function import io import re from glob import glob from os.path import basename from os.path import dirname from os.path import join from os.path import splitext from setuptools import find_packages from setuptools import setup def read ( * names , ** kwargs ): with io . open ( join ( dirname ( __file__ ), * names ), encoding = kwargs . get ( 'encoding' , 'utf8' ) ) as fh : return fh . read () setup ( name = 'nameless' , version = '1.644.11' , license = 'BSD-2-Clause' , description = 'An example package. Generated with cookiecutter-pylibrary.' , long_description = ' %s

%s ' % ( re . compile ( '^.. start-badges.*^.. end-badges' , re . M | re . S ) . sub ( '' , read ( 'README.rst' )), re . sub ( ':[a-z]+:`~?(.*?)`' , r '``\1``' , read ( 'CHANGELOG.rst' )) ), author = 'Ion \\ " \' el Cristian M \\ u0103rie \\ u0219' , author_email = 'contact@ionelmc.ro' , url = 'https://github.com/ionelmc/python-nameless' , packages = find_packages ( 'src' ), package_dir = { '' : 'src' }, py_modules = [ splitext ( basename ( path ))[ 0 ] for path in glob ( 'src/*.py' )], include_package_data = True , zip_safe = False , classifiers = [ # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 'Development Status :: 5 - Production/Stable' , 'Intended Audience :: Developers' , 'License :: OSI Approved :: BSD License' , 'Operating System :: Unix' , 'Operating System :: POSIX' , 'Operating System :: Microsoft :: Windows' , 'Programming Language :: Python' , 'Programming Language :: Python :: 2.7' , 'Programming Language :: Python :: 3' , 'Programming Language :: Python :: 3.5' , 'Programming Language :: Python :: 3.6' , 'Programming Language :: Python :: 3.7' , 'Programming Language :: Python :: 3.8' , 'Programming Language :: Python :: Implementation :: CPython' , 'Programming Language :: Python :: Implementation :: PyPy' , # uncomment if you test on these interpreters: # 'Programming Language :: Python :: Implementation :: IronPython', # 'Programming Language :: Python :: Implementation :: Jython', # 'Programming Language :: Python :: Implementation :: Stackless', 'Topic :: Utilities' , ], project_urls = { 'Changelog' : 'https://github.com/ionelmc/python-nameless/blob/master/CHANGELOG.rst' , 'Issue Tracker' : 'https://github.com/ionelmc/python-nameless/issues' , }, keywords = [ # eg: 'keyword1', 'keyword2', 'keyword3', ], python_requires = '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*' , install_requires = [ # eg: 'aspectlib==1.1.1', 'six>=1.7', ], extras_require = { # eg: # 'rst': ['docutils>=0.11'], # ':python_version=="2.6"': ['argparse'], }, setup_requires = [ 'pytest-runner' , ], entry_points = { 'console_scripts' : [ 'nameless = nameless.cli:main' , ] }, ) What's special about this: No exec or import trickery.

or trickery. Includes everything from src : packages or root-level modules.

: packages or root-level modules. Explicit encodings.