pyenvで指定したPythonのバージョンが実行されない例と解決策

# はじめに
この記事は [ZOZOテクノロジーズ #3 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/zozo_tech3) の19日目の記事です。
昨日の記事は [@BrunoB](https://qiita.com/BrunoB) さんによる [PAC4J : Security library for Play framework and Scala](https://qiita.com/BrunoB/items/fb53d545167067abc97e) でした。

# TL;DR

- Python のコマンドライン引数で渡したファイルパスと同じ階層に`.python-version`が存在する場合、 Python のバージョンが切り替わってしまう挙動を確認
- ファイルパスと同じ階層にある`.python-version`を削除するか、 環境変数`PYENV_VERSION`を設定することで解決

# 問題の挙動

初めて挙動に違和感を覚えたのは、`python hoge.py -a ./fuga/piyo.txt`コマンド実行時に、`ModuleNotFoundError`が出力された時です。
そのライブラリがインストールされていることは`pip list`コマンドで確認済だったので、上記コマンドでエラーが出力される理由が分かりませんでした。

詳しく調べてみると、`pyenv local`コマンドで指定したバージョンの Python が実行されず、システムデフォルトの Python が実行されており、システムデフォルトの Python には上記ライブラリがインストールされていないため、`ModuleNotFoundError`が発生していました。
どうやら、コマンドライン引数で渡した`./fuga/piyo.txt`と同じ階層にある`.python-version`が読み込まれて、実行されるPythonのバージョンが切り替わっているようでした。

# 問題となる挙動の再現
以下の環境、コード、コマンドで問題の挙動を再現します。

## 環境
AWS EC2 のインスタンスを用います。

* OS：Amazon Linux 2 AMI (HVM), SSD Volume Type 
* pyenv のバージョン：1.2.21（2020/12/19時点の最新バージョン）
* システムデフォルトの Python のバージョン：2.7.18
* pyenv で入れた Python のバージョン：3.7.6

## コード

今回実行するコード`print_version.py`の中身は以下の通りです。引数`arg1`の内容に関係なく、実行された Python のバージョンを`print(sys.version)`で表示するものです。
引数`arg1`は`main`関数内で使われませんが、この存在が今回のキモになります。


```python:print_version.py
import argparse
import sys

def main(arg1):
    print(sys.version)
    return()

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("-a", "--arg1")
    args = parser.parse_args()
    main(args.arg1)
```

そして、ディレクトリ構造は以下の通りです。
`print_version.py`と同じ階層に2個のディレクトリがあり、それらの中身は`.python-version`の有無以外が同じです。
ちなみに、`.python-version`は`pyenv local`コマンドが実行されたディレクトリに作成される隠しファイルなので、後述しますが`pyenv-test`ディレクトリで`pyenv local 3.7.6`コマンドを、`with-pv`ディレクトリで`pyenv local system`コマンドを実行しています。

```
pyenv-test
├── .python-version
├── print_version.py
├── with-pv
│   ├── .python-version
│   └── a.txt
└── without-pv
    └── a.txt
```

```:pyenv-test/.python-version
3.7.6
```

```:pyenv-test/with-pv/.python-version
system
```

```:a.txt
abcd
```

## コマンド
まずは以下のコマンドを実行し、出力結果も載せます。

```terminal
[ec2-user@******** pyenv-test]$ cd with-pv/

[ec2-user@******** with-pv]$ pyenv local system

[ec2-user@******** with-pv]$ python -V
Python 2.7.18

[ec2-user@******** with-pv]$ cd ../

[ec2-user@******** pyenv-test]$ pyenv local 3.7.6

[ec2-user@******** pyenv-test]$ python -V
Python 3.7.6
```

引き続き以下のコマンドを実行し、出力結果も載せます。
繰り返しになりますが、`print_version.py`は実行された Python のバージョンを表示するだけのコードです。

```terminal
[ec2-user@******** pyenv-test]$ python print_version.py -a ./without-pv/a.txt
3.7.6 (default, Dec  8 2020, 06:20:44)
[GCC 7.3.1 20180712 (Red Hat 7.3.1-9)]

[ec2-user@******** pyenv-test]$ python print_version.py -a ./with-pv/a.txt
2.7.18 (default, Aug 27 2020, 21:22:52)
[GCC 7.3.1 20180712 (Red Hat 7.3.1-9)]
```

中身が同じのファイルを`print_version.py`の引数として渡したはずですが、**実行された Python のバージョンが異なっています。**
`./with-pv/a.txt`をコマンドライン引数として渡した方は、pyenv で指定していないはずの Python 2.7.18 が実行されました。
両コマンドの違いは、コマンドライン引数として渡したファイルパスと同じ階層に、`.python-version`が有るか否かで、`.python-version`に記載された Python のバージョンが優先して実行されていると考えられます。

ちなみに、argparse の挙動による可能性も考えられるため、[Click](https://github.com/pallets/click) でも同様のことを試しましたが、結果は同じでした。

# 解決策
以下の2通りの方法があります。

## 1:環境変数`PYENV_VERSION`を設定する
実行される Python のバージョンを pyenv が決定する際に、最も優先されるのは環境変数`PYENV_VERSION`です（参考：[pyenv の本家リポジトリ](https://github.com/pyenv/pyenv#choosing-the-python-version)）。
`export PYENV_VERSION=3.7.6`コマンドを実行するか、`pyenv shell 3.7.6`コマンドを実行することでその環境変数を設定でき、実行したのが以下のものです。

```terminal:
[ec2-user@******** pyenv-test]$ pyenv shell 3.7.6

[ec2-user@******** pyenv-test]$ python print_version.py -a ./without-pv/a.txt
3.7.6 (default, Dec  8 2020, 06:20:44)
[GCC 7.3.1 20180712 (Red Hat 7.3.1-9)]

[ec2-user@******** pyenv-test]$ python print_version.py -a ./with-pv/a.txt
3.7.6 (default, Dec  8 2020, 06:20:44)
[GCC 7.3.1 20180712 (Red Hat 7.3.1-9)]
```

## 2:`.python-version`を削除する
[コマンド](#コマンド)の節で確認した通り、引数でファイルと同じ階層に`.python-version`が存在しなければ、`pyenv local`コマンドで指定された Python のバージョンで実行されます。

# 考察
[pyenv の本家リポジトリ](https://github.com/pyenv/pyenv#choosing-the-python-version)によると、コマンドライン引数で指定したファイルと同じ階層に存在する`.python-version`は、実行される Python のバージョンに影響を及ぼさないことが期待されます。
しかし、期待通りの挙動が見られなかったため、より詳しく仕様を把握する必要がありそうです。

# おわりに
自分が pyenv を使用している時にハマった箇所を本記事にまとめました。
`ModuleNotFoundError`の時点では、Python のバージョンが切り替わっていることに全く気が付かず、pyenv のインストールに失敗したことを長い間疑っており、解決まで時間がかかってしまいました。
更に、本記事ではコマンドライン引数が2個以上の場合を割愛しておりますが、私が当時書いていたコードはコマンドライン引数が4個ありました。これも解決まで時間がかかった要員の1つですが、なんとか原因を特定することができました。

明日は [@Horie1024](https://qiita.com/Horie1024) さんの記事となります。明日もお楽しみに。

# 追記(2021/4/8)
実は 2021/2/8 に本記事の内容で以下の issue を立てました。
https://github.com/pyenv/pyenv/issues/1807


この結果、少なくとも pyenv のバージョンが 1.2.23（2021/4/8時点の最新バージョン）では上記挙動が見られなくなりました。