First: environments

Installing packages


You often see these instructions to install a package:


                            pip install [package]         # Use only in virtual environment!
                            pip install --user [package]  # Almost never use
                        

🚨 Don't do this unless you know what you are doing!

Solution: virtual environments (libraries) and pipx (applications)


(and use system package managers like brew when possible)

Virtual environments


Use standard library venv module or virtualenv module:


                            python3 -m venv .venv
                        

Source with:


                            . .venv/bin/activate 
                        

Now pip install away!

What about conda?



                            # turn off the default environment
                            conda config --set auto_activate_base false  

                            conda env create -n some_name  # or use paths with `-p`
                            conda activate some_name
                            conda deactivate
                        

What about command line applications?


Use pipx, which creates new virtual environment for package


                            pipx install [package]
                            pipx inject ipython matplotlib
                        

(Python) packaging


module: Python file (.py) that contains definitions and statements.

package: a collection of modules in the same directory


Package directory must contain __init__.py for Python to "see" it*

* ... Not true with namespace packages

rescale function from Software Carpentry:


                            import numpy as np

                            def rescale(input_array):
                                """Rescales an array from 0 to 1.

                                Takes an array as input, and returns a corresponding array scaled 
                                so that 0 corresponds to the minimum and 1 to the maximum value 
                                of the input array.
                                """
                                L = np.min(input_array)
                                H = np.max(input_array)
                                output_array = (input_array - L) / (H - L)
                                return output_array
                        

Call it:


                            rescale(np.linspace(0, 100, 5))
                        

                            array([ 0.  ,  0.25,  0.5 ,  0.75,  1.  ])
                        

Package in six lines:

  • mkdir package
  • cd package
  • git init
  • mkdir -p src/rescale tests docs
  • touch src/rescale/__init__.py src/rescale/rescale.py
  • touch pyproject.toml

Contents of pyproject.toml



                            # contents of pyproject.toml
                            [build-system]
                            requires = ["hatchling"]
                            build-backend = "hatchling.build"
                            
                            [project]
                            name = "package"
                            version = "0.1.0"
                        

Package file structure:



                            .
                            ├── docs/
                            ├── pyproject.toml
                            ├── src/
                            │   └── package/
                            │   │   ├── __init__.py
                            │   │   └── rescale.py
                            └── tests/
                        

Install and import package


Install in editable mode:


                            $ pip install -e .
                        

Import and call:


                            import numpy as np
                            from package.rescale import rescale

                            rescale(np.linspace(0, 100, 5))
                        

Output:


                            array([0.  , 0.25, 0.5 , 0.75, 1.  ])
                        

compphys package structure:



                            src/
                            |-- compphys/
                            |   |-- __init__.py
                            |   |-- constants.py
                            |   |-- physics.py
                            |   |-- more/
                            |   |   |-- __init__.py
                            |   |   |-- morephysics.py
                            |   |   |-- evenmorephysics.py
                            |-- assets/
                            |   |-- data.txt
                            |   |-- orphan.py
                            tests/
                            |-- test_physics.py
                            |-- test_morephysics.py
                            docs/
                            ...
                        

compphys contents:


  • __init__.py: tells Python this is a package; typically empty. Executed before any other modules imported.
  • Two modules: constants.py and physics.py
  • more is a submodule
  • assets is a subdirectory; not a submodule since it doesn't have __init__.py (orphan.py is unreachable!)
  • tests: subdirectory with tests

Importing submodules


Use attribute access operator: .


                            import compphys.constants
                            import compphys.more.morephysics

                            two_pi = 2 * compphys.constants.pi
                        

These are absolute imports

Explicit relative imports


From physics.py:


                            from . import constants
                            from .constants import pi, h
                            from .more import morephysics
                        

From evenmorephysics.py:


                            from . import morephysics
                            from .. import constants
                            from ..constants import pi, h
                        

Important: while easy to create new functions and modules, don't reinvent the wheel!


Rely on the Python standard library, NumPy, SciPy, etc. as much as you need.


                            from time import sleep
                            print("This is my file to demonstrate best practices.")

                            def process_data(data):
                                print("Beginning data processing...")
                                modified_data = data + " that has been modified"
                                sleep(3)
                                print("Data processing finished.")
                                return modified_data

                            def read_data_from_web():
                                print("Reading data from the Web")
                                data = "Data from the web"
                                return data

                            def write_data_to_database(data):
                                print("Writing data to a database")
                                print(data)

                            def main():
                                data = read_data_from_web()
                                modified_data = process_data(data)
                                write_data_to_database(modified_data)

                            if __name__ == "__main__":
                                main()
                        

Source: https://realpython.com/python-main-function/

Other files that belong with your package

  • Description and other information about package
  • Terms and conditions under which software can be downloaded, used, and/or modified
  • Keeping track of changes

Create a README


                            touch README.md
                        

A README is a form of software documentation, and should contain at minimum:

  • the name of your software package
  • a brief description of what your software does or provides
  • installation instructions
  • a brief usage example
  • the type of software license (with more information in a separate LICENSE file, described next)

In addition, a README may also contain:

  • badges near the top that quickly show key information, such as the latest version, whether the tests are currently passing
  • information about how people can contribute to your package
  • a code of conduct for people interacting around your project (in GitHub Issues or Pull Requests, for example)
  • contact information for authors and/or maintainers

                        # Package

                        `package` is a simple Python library that contains a single function 
                        for rescaling arrays.

                        ## Installation

                        Download the source code and use the package manager 
                        [pip](https://pip.pypa.io/en/stable/) to install `package`:

                        ```bash
                        pip install .
                        ```

                        ## Usage

                        ```python
                        import numpy as np
                        from package.rescale import rescale

                        # rescales over 0 to 1
                        rescale(np.linspace(0, 100, 5))
                        ```

                        ## Contributing
                        Pull requests are welcome. For major changes, please open an issue 
                        first to discuss what you would like to change.

                        Please make sure to update tests as appropriate.

                        ## License
                        TBD
                        

Choose a software license


More on this later... for now:


                            touch LICENSE
                        

Copy-paste the BSD 3-clause license:


                            BSD 3-Clause License

                            Copyright (c) [year], [fullname]

                            Redistribution and use in source and binary forms, with or without
                            modification, are permitted provided that the following conditions are met:

                            1. Redistributions of source code must retain the above copyright notice, this
                            list of conditions and the following disclaimer.

                            2. Redistributions in binary form must reproduce the above copyright notice,
                            this list of conditions and the following disclaimer in the documentation
                            and/or other materials provided with the distribution.

                            3. Neither the name of the copyright holder nor the names of its
                            contributors may be used to endorse or promote products derived from
                            this software without specific prior written permission.

                            THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
                            AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
                            IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
                            DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
                            FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
                            DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
                            SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
                            CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
                            OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
                            OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
                            

Keep a CHANGELOG


Over time, your package will change as you fix bugs, add features, and make improvements.

Technically, your Git history should contain all the records of changes, but in practice this isn't very helpful for functionality changes.

Instead, keep a CHANGELOG that is a human-readable record of major changes between each version.

Contents of CHANGELOG.md


                        # Changelog
                        All notable changes to this project will be documented in this file.

                        The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
                        and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

                        ## [Unreleased]

                        ## [0.1.0] - 2015-10-06
                        ### Added
                        - Answer "Should you ever rewrite a change log?".

                        ### Changed
                        - Improve argument against commit logs.
                        - Start following [SemVer](http://semver.org) properly.

                        ## [0.0.8] - 2015-02-17
                        ### Changed
                        - Update year to match in every README example.
                        - Reluctantly stop making fun of Brits only, since most of the world
                            writes dates in a strange way.

                        ### Fixed
                        - Fix typos in recent README changes.
                        - Update outdated unreleased diff link.

                        ## [0.0.7] - 2015-02-16
                        ### Added
                        - Link, and make it obvious that date format is ISO 8601.

                        ### Changed
                        - Clarified the section on "Is there a standard change log format?".

                        ### Fixed
                        - Fix Markdown links to tag comparison URL with footnote-style links.

                        ## [0.0.6] - 2014-12-12
                        ### Added
                        - README section on "yanked" releases.

                        ## [0.0.5] - 2014-08-09
                        ### Added
                        - Markdown links to version tags on release headings.
                        - Unreleased section to gather unreleased changes and encourage note
                        keeping prior to releases.

                        ## [0.0.4] - 2014-08-09
                        ### Added
                        - Better explanation of the difference between the file ("CHANGELOG")
                        and its function "the change log".

                        ### Changed
                        - Refer to a "change log" instead of a "CHANGELOG" throughout the site
                        to differentiate between the file and the purpose of the file — the
                        logging of changes.

                        ### Removed
                        - Remove empty sections from CHANGELOG, they occupy too much space and
                        create too much noise in the file. People will have to assume that the
                        missing sections were intentionally left out because they contained no
                        notable changes.

                        ## [0.0.3] - 2014-08-09
                        ### Added
                        - "Why should I care?" section mentioning The Changelog podcast.

                        ## [0.0.2] - 2014-07-10
                        ### Added
                        - Explanation of the recommended reverse chronological release ordering.

                        ## 0.0.1 - 2014-05-31
                        ### Added
                        - This CHANGELOG file to hopefully serve as an evolving example of a
                            standardized open source project CHANGELOG.
                        - CNAME file to enable GitHub Pages custom domain
                        - README now contains answers to common questions about CHANGELOGs
                        - Good examples and basic guidelines, including proper date formatting.
                        - Counter-examples: "What makes unicorns cry?"

                        [Unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v1.0.0...HEAD
                        [0.1.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.8...v0.1.0
                        [0.0.8]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.7...v0.0.8
                        [0.0.7]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.6...v0.0.7
                        [0.0.6]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.5...v0.0.6
                        [0.0.5]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.4...v0.0.5
                        [0.0.4]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.3...v0.0.4
                        [0.0.3]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.2...v0.0.3
                        [0.0.2]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.0.1...v0.0.2
                        

                        pyproject.toml
                        src/
                        |-- compphys/
                        |   |--  __init__.py
                        |   |-- _version.py
                        |   |-- constants.py
                        |   |-- physics.py
                        |   |-- more/
                        |   |   |-- __init__.py
                        |   |   |-- morephysics.py
                        |   |   |-- evenmorephysics.py
                        assets/
                        |-- data.txt
                        |-- orphan.py
                        tests/
                        |-- test_physics.py
                        |-- test_morephysics.py
                        docs/
                        LICENSE
                        README.md
                        CHANGELOG.md
                        CONTRIBUTING.md 
                        CODE_OF_CONDUCT.md
                        

Metadata: other information contained in pyproject.toml

Name



                            name = "some_project"
                        

Version



                            version = "1.2.3"
                            version = "0.2.1b1"
                        

Can also have specified elsewhere with dynamic = ["version"]

Description


                            description = "This is a very short summary of a very cool project."
                        

Readme


                            readme = "README.md"
                        

Keywords


                            keywords = ["example", "tutorial"]
                        

Authors and maintainers



                            authors = [
                                {name="Me Myself", email="email@mail.com"},
                                {name="You Yourself", email="email2@mail.com"},
                            ]
                            maintainers = [
                                {name="It Itself", email="email3@mail.com"},
                            ]
                        

URLs



                            # Inline form
                            urls.Homepage = "https://pypi.org"
                            urls."Source Code" = "https://pypi.org"

                            # Sectional form
                            [project.urls]
                            Homepage = "https://pypi.org"
                            "Source Code" = "https://pypi.org"
                        

Others: Documentation, Bug Tracker, Changelog, Discussions, and Chat

Classifiers



                            classifiers = [
                                "Development Status :: 5 - Production/Stable",
                                "Intended Audience :: Developers",
                                "Intended Audience :: Science/Research",
                                "License :: OSI Approved :: BSD License",
                                "Operating System :: OS Independent",
                                "Programming Language :: Python",
                                "Programming Language :: Python :: 3",
                                "Programming Language :: Python :: 3 :: Only",
                                "Programming Language :: Python :: 3.8",
                                "Programming Language :: Python :: 3.9",
                                "Programming Language :: Python :: 3.10",
                                "Programming Language :: Python :: 3.11",
                                "Topic :: Scientific/Engineering",
                                "Topic :: Scientific/Engineering :: Information Analysis",
                                "Topic :: Scientific/Engineering :: Mathematics",
                                "Topic :: Scientific/Engineering :: Physics",
                                "Typing :: Typed",
                            ]
                        

Functional metadata



                            requires-python = ">=3.8"

                            dependencies = [
                                "numpy>=1.18",
                            ]

                            [project.optional-dependenices]
                            test = ["pytest>=6"]
                            check = ["flake8"]
                            plot = ["matplotlib"]

                            [project.scripts]
                            project-cli = "project.__main__:main"
                        

pyproject.toml


                        [build-system]
                        requires = ["hatchling"]
                        build-backend = "hatchling.build"

                        [project]
                        name = "package"
                        version = "0.0.1"
                        authors = [
                            { name="Example Author", email="author@example.com" },
                        ]
                        description = "A small example package"
                        readme = "README.md"
                        requires-python = ">=3.8"
                        classifiers = [
                            "Programming Language :: Python :: 3",
                            "License :: OSI Approved :: MIT License",
                            "Operating System :: OS Independent",
                        ]

                        [project.urls]
                        "Homepage" = "https://github.com/pypa/sampleproject"
                        "Bug Tracker" = "https://github.com/pypa/sampleproject/issues"