Installing CPython as a macOS framework

About framework installs

When using Python on macOS, installing Python as a framework is generally preferred, unless macOS specific features will not be used. In a non framework install of Python on macOS, unexpected behavior can occur.

An example of this behavior can be seen by running pywebview (compiled with Cocoa bindings) with a Python install that was compiled with pyenv's default options. Pywebview still loads the requested page in Safari, but when text is entered into a field (such as a https://duckduckgo.com search box), the text appears in the terminal window instead of the Safari window. Using a framework install of problem resolves this issue.

Installing Python as a framework

Homebrew

There are multiple ways to install Python as a framework with Homebrew being the simplest. The default formula compiles Python as a framework install, places Python3 in $PATH and even installs the "extras". This will likely be adequate for most users.

Assuming Homebrew is configured, installation is a single command.

brew install python

I avoid using Homebrew for Python management since I have multiple versions of Python 3 installed and Homebrew follows the Henry Ford choice model with Python 3 versions.

Pyenv

Pyenv is a Python version management utility for Linux and Unix based systems. Since Pyenv is essentially a bunch of BASH scripts, it avoids being yet another Python dependency. I do not use pyenv as a way to switch between Python versions but rather as an incredibly simple way of install python-build.

I find the simplest method to install pyenv (including python-build) and other needed libraries is to use Homebrew.

brew install pyenv
brew install openssl
brew install readline

Once pyenv has been installed, specific options need to be passed in to perform a framework build and installation. Instead of trying to remember these options, I have created a script to do the work for me.

Pyenv framework installation script - no extras

The script below takes a single argument: the CPython version to build.

To view CPython versions available for installation via pyenv, run pyenv install -l. The CPython versions are at the top of the list with just a simple version number, such as 3.6.5.

#!/bin/sh
# Compiles and installs CPython framework WITHOUT extras
# Author: Shawn Hensley

PYENV_INSTALL_VER="${1}"

if [ -z "${PYENV_INSTALL_VER}" ] ; then
    printf "Please specify the python version to install..\n"
    exit 1
fi

# Setup paths
PYEROOT="$(pyenv root)"
FWINSTPATH="${PYEROOT}/versions/${PYENV_INSTALL_VER}"
PYAPPINSTPATH="${PYEROOT}/versions/${PYENV_INSTALL_VER}/Applications"

# Perform exports for pyenv usage
export PYENV_INSTALL_VER
export PYTHON_CONFIGURE_OPTS="--enable-framework=${FWINSTPATH}"
export MAKE_INSTALL_OPTS="PYTHONAPPSDIR=${PYAPPINSTPATH}"

# Perform build and install.  No extras are installed
pyenv install "${PYENV_INSTALL_VER}"

exit

Previous versions of the script included the options below. Pyenv now automatically detects Homebrew installed OpenSSL and readline libraries, so these options are no longer required. If CFLAGS and/or LDFLAGS are needed, the below code can instead be used as an example of how to pass those into pyenv.

...
CFLAGS="-I$(brew --prefix openssl)/include" \
LDFLAGS="-L$(brew --prefix openssl)/lib" \
pyenv install "${PYENV_INSTALL_VER}"
...

Pyenv framework installation script - with extras

While I personally do not use the "Extras" from the framework install, this may be useful for someone (such as DenverCoder9) who is searching for "frameworkinstallextras with pyenv".

To perform "frameworkinstallextras", the source --keep flag must be passed to pyenv to avoid automatic removal of the source folder. The source folder should no longer be needed once "frameworkinstallextras" has completed, so it is removed at the end of the script. Comment out the rm line to keep the source folder.

#!/bin/sh
# Compiles and installs CPython framework with extras
# Author: Shawn Hensley

PYENV_INSTALL_VER="${1}"

if [ -z "${PYENV_INSTALL_VER}" ] ; then
    printf "Please specify the python version to install..\n"
    exit 1
fi

# Setup paths
PYEROOT="$(pyenv root)"
FWINSTPATH="${PYEROOT}/versions/${PYENV_INSTALL_VER}"
PYAPPINSTPATH="${PYEROOT}/versions/${PYENV_INSTALL_VER}/Applications"

# Perform exports for pyenv usage
export PYENV_INSTALL_VER
export PYTHON_CONFIGURE_OPTS="--enable-framework=${FWINSTPATH}"
export MAKE_INSTALL_OPTS="PYTHONAPPSDIR=${PYAPPINSTPATH}"

# Perform build and install. Keep source dir for "make frameworkinstallextras"
pyenv install --keep --verbose "${PYENV_INSTALL_VER}"

# Install framework extras
SOURCEPATH="${PYEROOT}/sources/${PYENV_INSTALL_VER}/Python-${PYENV_INSTALL_VER}"
cd "${SOURCEPATH}" && make frameworkinstallextras
cd "${HOME}"

# Remove source folder
rm -r "${SOURCEPATH:?}"

exit

Final steps

Once the install is complete, virtual environments can be created using standard methods. An upcoming post will detail how I manage my virtual environments but the short version is: I use Python 3's venv module.