{"id":401,"date":"2023-04-20T17:23:52","date_gmt":"2023-04-20T17:23:52","guid":{"rendered":"https:\/\/blogs-staging.imperial.ac.uk\/research-software-engineering\/?p=401"},"modified":"2024-05-22T08:00:08","modified_gmt":"2024-05-22T08:00:08","slug":"python-development-on-m1-macs","status":"publish","type":"post","link":"https:\/\/blogs-staging.imperial.ac.uk\/research-software-engineering\/2023\/04\/20\/python-development-on-m1-macs\/","title":{"rendered":"Python Development on M1 Macs"},"content":{"rendered":"<h1>Apple Silicon Python Setup<\/h1>\n<p>I recently received a new MacBook Pro for work. Great! The only catch is that <a href=\"https:\/\/en.wikipedia.org\/wiki\/MacBook_Pro_(Intel-based)\">Apple discontinued their Intel-based line of MacBook Pros in 2021<\/a> and their new line of laptops use the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Apple_silicon#M-series_coprocessors\">Apple silicon M-series coprocessors<\/a>. This might not seem like a problem at first, but the Apple silicon processors use <a href=\"https:\/\/en.wikipedia.org\/wiki\/ARM_architecture_family\">ARM-architecture<\/a> instead of <a href=\"https:\/\/en.wikipedia.org\/wiki\/X86\">Intel&#8217;s x86 architecture<\/a>. For Python development, this is a potential problem because not all Python packages are installable for ARM architectures.<\/p>\n<p><!--more--><\/p>\n<p>There is a possibly simple solution, in May 2022 Anaconda released a\u00a0<a href=\"https:\/\/www.anaconda.com\/blog\/new-release-anaconda-distribution-now-supporting-m1\">distribution supporting Apple silicon<\/a>. However, a few years ago I chose to move away from using Anaconda to manage my Python virtual environments in favour of a more lightweight solution that I have more control over.<\/p>\n<p>For years I have used the inbuilt <a href=\"https:\/\/docs.python.org\/3\/library\/venv.html\">venv module<\/a> for creating virtual environments in combination with <a href=\"https:\/\/github.com\/pyenv\/pyenv\">pyenv<\/a> for installing multiple differnet versions of Python itself. With some help from <a href=\"https:\/\/towardsdatascience.com\/how-to-use-manage-multiple-python-versions-on-an-apple-silicon-m1-mac-d69ee6ed0250\">this blog post<\/a> I was able to combine <code>pyenv<\/code> with <a href=\"https:\/\/support.apple.com\/en-gb\/HT211861\">Rosetta2<\/a> (Apple&#8217;s x86 emulation tool that allows x86-compiled software to run on your Mac) to create a smooth setup to switch between different architecture versions of Python.<\/p>\n<p>The steps to set this up are below.<\/p>\n<h2>Installing pyenv<\/h2>\n<h3>Default ARM64 Installation<\/h3>\n<p>This section provides instructions on how to install Python using the default ARM64 architecture and <code>pyenv<\/code>. This will cover most of the use cases for developing in Python on macOS. Hopefully, in a few years this will be all that is required.<\/p>\n<p>The best way to install <code>pyenv<\/code>\u00a0is through <a href=\"https:\/\/brew.sh\">Homebrew<\/a>. This is <a href=\"https:\/\/github.com\/pyenv\/pyenv#installation\">recommended by <code>pyenv<\/code><\/a> and takes care of a lot of the additional <a href=\"https:\/\/github.com\/pyenv\/pyenv#set-up-your-shell-environment-for-pyenv\">shell environment setup<\/a>. Installed with:<\/p>\n<pre><code>$ \/bin\/bash -c \"$(curl -fsSL https:\/\/raw.githubusercontent.com\/Homebrew\/install\/HEAD\/install.sh)\"<\/code><\/pre>\n<p>This defaults to install it in <code>\/opt\/homebrew<\/code>. If you are prompted to install Xcode Commandline tools, say yes. You can now install many tools using the <code>brew<\/code> command from Homebrew.<\/p>\n<p>Setup the <a href=\"https:\/\/github.com\/pyenv\/pyenv\/wiki#suggested-build-environment\">recommended build environment<\/a> for <code>pyenv<\/code>:<\/p>\n<pre><code>$ brew install openssl readline sqlite3 xz zlib tcl-tk<\/code><\/pre>\n<p>Then install <code>pyenv<\/code>:<\/p>\n<pre><code>$ brew install pyenv<\/code><\/pre>\n<p>And finally, run to following to initialise your shell for <code>pyenv<\/code> every time you open it:<\/p>\n<pre><code>$ echo 'eval \"$(pyenv init -)\"' &gt;&gt; ~\/.zshrc<\/code><\/pre>\n<p>Restart your terminal to enable the new <code>.zshrc<\/code> configuration.<\/p>\n<p><em><strong>Note:\u00a0<\/strong>zsh has been the default login shell macOS since before the M1 chip was released, so I am assuming you are using zsh. If not, replace <code>.zshrc<\/code> with <code>.bashrc<\/code> whenever I mention it. Just be aware of potential <a href=\"https:\/\/apple.stackexchange.com\/questions\/361870\/what-are-the-practical-differences-between-bash-and-zsh\">differences in syntax<\/a>.<\/em><\/p>\n<p>You can then install a desired version of python (see list of possible versions with <code>pyenv install --list<\/code>), and set it as the global (default) python version. I am installing 3.10.9, but you can install whichever version you want:<\/p>\n<pre><code>\r\n$ pyenv install 3.10.9\r\n$ pyenv global 3.10.9\r\n<\/code><\/pre>\n<p>You can verify that your python version is the one you just set and the platform architecture is using the inbuilt ARM architecture with:<\/p>\n<pre><code>\r\n$ python --version\r\nPython 3.10.9\r\n$ python -c \"import platform; print(platform.machine())\"\r\narm64\r\n<\/code><\/pre>\n<p>And you&#8217;re done! If all the packages you use are installable with arm64 architecture you can stop here, but if you run into this kind of error, read on&#8230;<\/p>\n<pre><code>\r\n$ pip install &lt;package&gt;\r\nERROR: Could not find a version that satisfies the requirement &lt;package&gt;\r\nERROR: No matching distribution found for &lt;package&gt;\r\n<\/code><\/pre>\n<h3>x86 Installation with Rosetta2<\/h3>\n<p>This section provides instructions on how to install Python using the x86 architecture emulated by Rosetta2.<\/p>\n<p>Commands can be run under emulated x86 architecture using Rosetta 2. Install it with:<\/p>\n<pre><code>\r\n$ softwareupdate --install-rosetta\r\n<\/code><\/pre>\n<p>You can run shell commands under x86 using the <code>arch -x86_64<\/code> command:<\/p>\n<pre><code>\r\n$ uname -m\r\narm64\r\n$ arch -x86_64 uname -m\r\nx86_64\r\n<\/code><\/pre>\n<p>I will need to install an x86 version of Homebrew and <code>pyenv<\/code>. I had <a href=\"https:\/\/github.com\/Homebrew\/discussions\/discussions\/600\">this permissions issue<\/a> when installing this, so first fix this with (need sudo\/admin permissions):<\/p>\n<pre><code>\r\n$ sudo chown -R $(whoami) \/usr\/local\/share\/zsh \/usr\/local\/share\/zsh\/site-functions\r\n<\/code><\/pre>\n<p>Then install the x86 version of Homebrew. It is the same install command as above, but by using the Rosetta2 emulation Homebrew knows to install the x86 version:<\/p>\n<pre><code>\r\n$ arch -x86_64 \/bin\/bash -c \"$(curl -fsSL https:\/\/raw.githubusercontent.com\/Homebrew\/install\/HEAD\/install.sh)\"\r\n<\/code><\/pre>\n<p>This will install brew in <code>\/usr\/local\/bin\/brew<\/code>, for which you will need to configure your shell environment. Notice how the location of the <code>brew<\/code> executable changes from the default arm64 location, to the new x86 location:<\/p>\n<pre><code>\r\n$ which brew\r\n\/opt\/homebrew\/bin\/brew\r\n$ eval \"$(arch --x86_64 \/usr\/local\/bin\/brew shellenv)\"\r\n$ which brew\r\n\/usr\/local\/bin\/brew\r\n<\/code><\/pre>\n<p><em><strong>Note:<\/strong>\u00a0The command <code>eval \"$(arch --x86_64 \/usr\/local\/bin\/brew shellenv)\"<\/code> will need to be run every time you need to use the x86 version of Homebrew to install. More on this later.<\/em><\/p>\n<p>Now, you can install <code>pyenv<\/code> using the x86 version of Homebrew, don&#8217;t forget to still set up the <a href=\"https:\/\/github.com\/pyenv\/pyenv\/wiki#suggested-build-environment\">recommended build environment<\/a> for <code>pyenv<\/code>:<\/p>\n<pre><code>\r\n$ arch --x86_64 brew install openssl readline sqlite3 xz zlib tcl-tk\r\n$ arch --x86_64 brew install pyenv\r\n<\/code><\/pre>\n<p>You now have all the tools to install multiple versions of Python in different architectures! However there are some teething problems and clunky user-experience that still needs to be set up before we do so.<\/p>\n<h2>Set up a smooth user experience<\/h2>\n<h3>Easily switch architectures<\/h3>\n<p>Set up some useful aliases by adding the following to your <code>.zshrc<\/code>:<\/p>\n<pre><code>\r\n# Alias for switching terminal architecture\r\nalias x86='arch -x86_64 zsh --login'\r\nalias arm64='arch -arm64 zsh --login'\r\n\r\n# Edit prompt name so you know the current architecture\r\nPROMPT=\"$(uname -m) $PROMPT\"\r\n<\/code><\/pre>\n<p>Restart your shell, you should now see <code>arm64<\/code> at the front of your prompt, and it will change if you use the newly-created <code>x86<\/code> alias to switch architecture:<\/p>\n<pre><code>\r\narm64 $ uname -m\r\narm64\r\narm64 $ x86\r\nx86_64 $ uname -m\r\nx86_64\r\n<\/code><\/pre>\n<p>You can return to the ARM64 shell with <code>exit<\/code> or with the <code>arm64<\/code> alias.<\/p>\n<h3>Python version name suffix<\/h3>\n<p>One limitation of using <code>pyenv<\/code> to install different architecture versions of python is that it doesn&#8217;t allow you to give your different python versions custom names. So if I want to now install an x86 version of Python 3.10.9 (the same as above), I cannot do so without overwriting the ARM64 installation:<\/p>\n<pre><code>\r\narm64 $ arch -x86_64 pyenv install 3.10.9\r\npyenv: \/Users\/adalessa\/.pyenv\/versions\/3.10.9 already exists\r\ncontinue with installation? (y\/N) N\r\n<\/code><\/pre>\n<p>To solve this issue, I adapted the broken <a href=\"https:\/\/github.com\/s1341\/pyenv-alias\"><code>pyenv-alias<\/code><\/a> plugin to create <a href=\"https:\/\/github.com\/AdrianDAlessandro\/pyenv-suffix\"><code>pyenv-suffix<\/code><\/a>, which allows you specify a suffix to append to the version name of a <code>pyenv<\/code>-installed version of Python.<\/p>\n<p>Install it with:<\/p>\n<pre><code>\r\n$ git clone https:\/\/github.com\/AdrianDAlessandro\/pyenv-suffix.git $(pyenv root)\/plugins\/pyenv-suffix\r\n<\/code><\/pre>\n<p>Now, if you set the <code>PYENV_VERSION_SUFFIX<\/code> environment variable, it will be appended to any version you install with <code>pyenv<\/code>.<\/p>\n<h3>Shell configuration<\/h3>\n<p>The last thing that needs to be fixed is ensuring that the shell is configured correctly for the architecture you are using. Add the following to your <code>.zshrc<\/code> and open a new terminal:<\/p>\n<pre><code>\r\n# Alias for x86 version of Homebrew and pyenv\r\nif [ $(uname -m) = \"x86_64\" ]; then\r\neval \"$(arch --x86_64 \/usr\/local\/bin\/brew shellenv)\"\r\nalias pyenv='PYENV_VERSION_SUFFIX=\"x86\" \/usr\/local\/bin\/pyenv'\r\nfi\r\n<\/code><\/pre>\n<p>This will mean everytime you open a new x86 terminal, the Homebrew shell environment will be configured, and the <code>PYENV_VERSION_SUFFIX<\/code> envirtonment variable will be set. This should mean seemless installation of anything with Homebrew and <code>pyenv<\/code>.<\/p>\n<h3>Install an x86 version of Python!<\/h3>\n<p>This is as simple as entering an x86 shell with the <code>x86<\/code> alias and installing the desired Python version. First, check what versions of Python you have installed, it should look something like this:<\/p>\n<pre><code>\r\narm64 $ pyenv versions\r\nsystem\r\n* 3.10.9 (set by \/Users\/&lt;username&gt;\/.pyenv\/version)\r\n<\/code><\/pre>\n<p>Then, enter the x86 shell and install the desired version:<\/p>\n<pre><code>\r\narm64 $ x86\r\nx86_64 $ pyenv install 3.10.9\r\npyenv: \/Users\/adalessa\/.pyenv\/versions\/3.10.9 already exists\r\ncontinue with installation? (y\/N) y\r\nInstalling at \/Users\/adalessa\/.pyenv\/versions\/3.10.9x86\r\npython-build: use openssl from homebrew\r\npython-build: use readline from homebrew\r\nDownloading Python-3.10.9.tar.xz...\r\n-&gt; https:\/\/www.python.org\/ftp\/python\/3.10.9\/Python-3.10.9.tar.xz\r\nInstalling Python-3.10.9...\r\npython-build: use readline from homebrew\r\npython-build: use zlib from xcode sdk\r\nInstalled Python-3.10.9 to \/Users\/adalessa\/.pyenv\/versions\/3.10.9x86\r\n<\/code><\/pre>\n<p>Now you should see the new version installed with the &#8220;x86&#8221; suffix appended (note I have exited the x86 shell, but it doesn&#8217;t matter):<\/p>\n<pre><code>\r\narm64 $ pyenv versions\r\nsystem\r\n* 3.10.9 (set by \/Users\/&lt;username&gt;\/.pyenv\/version)\r\n3.10.9x86\r\n<\/code><\/pre>\n<p>How do you use it? There are three ways <a href=\"https:\/\/github.com\/pyenv\/pyenv#switch-between-python-versions\">described in the <code>pyenv<\/code> GitHub Page<\/a>. The simplest of which is the <code>pyenv shell<\/code> command:<\/p>\n<pre><code>\r\narm64 $ python -c \"import platform; print(platform.machine())\"\r\narm64\r\narm64 $ pyenv shell 3.10.9x86\r\narm64 $ python -c \"import platform; print(platform.machine())\"\r\nx86_64\r\n<\/code><\/pre>\n<p>Although my preferred method is to set the local version of python on a per-project basis. This is so that the python version automatically changes when you enter a project directory, and also because using <code>pyenv shell<\/code> persists until you restart your shell.<\/p>\n<p>Open a new terminal. Enter your project directory and set the local version:<\/p>\n<pre><code>\r\narm64 $ cd my_project\r\narm64 $ python -c \"import platform; print(platform.machine())\"\r\narm64\r\narm64 $ pyenv local 3.10.9x86\r\narm64 $ python -c \"import platform; print(platform.machine())\"\r\nx86_64\r\n<\/code><\/pre>\n<p>You will now see a <code>.python-version<\/code> file added to your directory and that version of Python will be used whenever you are in that directory!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Apple Silicon Python Setup I recently received a new MacBook Pro for work. Great! The only catch is that Apple discontinued their Intel-based line of MacBook Pros in 2021 and their new line of laptops use the Apple silicon M-series coprocessors. This might not seem like a problem at first, but the Apple silicon processors [&hellip;]<\/p>\n","protected":false},"author":1737,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[317604,317607,317608,317606,317605,268274,317609],"class_list":["post-401","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-apple","tag-arm","tag-development","tag-intel","tag-macos","tag-python","tag-software"],"_links":{"self":[{"href":"https:\/\/blogs-staging.imperial.ac.uk\/research-software-engineering\/wp-json\/wp\/v2\/posts\/401","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blogs-staging.imperial.ac.uk\/research-software-engineering\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogs-staging.imperial.ac.uk\/research-software-engineering\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogs-staging.imperial.ac.uk\/research-software-engineering\/wp-json\/wp\/v2\/users\/1737"}],"replies":[{"embeddable":true,"href":"https:\/\/blogs-staging.imperial.ac.uk\/research-software-engineering\/wp-json\/wp\/v2\/comments?post=401"}],"version-history":[{"count":30,"href":"https:\/\/blogs-staging.imperial.ac.uk\/research-software-engineering\/wp-json\/wp\/v2\/posts\/401\/revisions"}],"predecessor-version":[{"id":440,"href":"https:\/\/blogs-staging.imperial.ac.uk\/research-software-engineering\/wp-json\/wp\/v2\/posts\/401\/revisions\/440"}],"wp:attachment":[{"href":"https:\/\/blogs-staging.imperial.ac.uk\/research-software-engineering\/wp-json\/wp\/v2\/media?parent=401"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs-staging.imperial.ac.uk\/research-software-engineering\/wp-json\/wp\/v2\/categories?post=401"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs-staging.imperial.ac.uk\/research-software-engineering\/wp-json\/wp\/v2\/tags?post=401"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}