Python file I/O on Windows and Linux are two different things
When using threads you can run into problems caused by different behaviour of file I/O functions.

I have a Python program running fine on Linux. Few months ago I wanted to run this on Windows.
This was the first time I used Python on Windows. Install Python app, create virtual environment, copy and run. No problems ... oh but there was a problem. My session sometimes disappeared ... WTF! I noticed the problem by repeatly hitting F5 in a very short time. Time for a thorough investigation.
The application and the session key-value store
The Python app is a Flask application. It uses the file system interface of Flask-Session to store sessions as files. Flask-Session uses another PyPi package to handle all file I/O. As you can imagine this package implements a key-value store.
There are two main methods in this package:
- set(key, value), to write/update the session data
- get(key), to get the session data
In the package, the set(key, value) method creates a temporary file and then calls the Python os.replace() function to replace the session file. The get(key) method calls the Python read function.
Test using threads
When you run Flask in development mode, you will see a lot of requests to the session store because Flask also serves images, CSS files, etc. In a production environment where you serve static content via a webserver you are less likely to run into this problem, but it is still there!
This is how I came up with the idea to write a small test using threads.
import cachelib
import threading
fsc = cachelib.file.FileSystemCache('.')
def set_get(i):
fsc.set('key', 'val')
val = fsc.get('key')
for i in range(10):
t = threading.Thread(target=set_get, args=(i,))
t.start()
On Linux there were no errors, nothing. But on Windows this raised randomly generated exceptions:
[WinError 5] Access is denied
...
[Errno 13] Permission denied
The [WinError 5] exception was generated by the Python os.replace() function. The [Errno 13] exception was generated by the Python read() function.
What is going on here?
File I/O on Windows and Linux are two different things
I assumed that Python would shield me from platform specific implementations. It does so in many functions but not in all. Especially when using threads you can run into problems caused by different behaviour of file I/O functions.
From Python Bug Tracker Issue46003:
As they say, there's no such thing as 'portable software', only 'software that has been ported'. Especially in an area like file I/O: once you move beyond simple 'one process opens, writes, and closes' and another process then 'opens, reads, and closes', there are a lot of platform-specific issues. Python does not try to abstract away all possible file I/O issues. |
Python read() function
In the library I am using, the get(key) method uses the Python read() function.
On Linux it is sufficient to put the read() function in a try-except:
try:
with open(f, 'r') as fo:
return fo.read()
except Exception as e:
return None
The function will wait until the data is available. An exception will only be raised if there is a timeout, which on most Linux systems is 60 seconds, or some other unexpected error.
On Windows this will fail immediately if the file is accessed by another thread. To create the same behaviour we have with Linux we must add retries and a delay, for example:
max_sleep_time = 10
total_sleep_time = 0
sleep_time = 0.02
while total_sleep_time < max_sleep_time:
try:
with open(f, 'r') as fo:
return fo.read()
except OSError as e:
errno = getattr(e, 'errno', None)
if errno == 13:
# permission error
time.sleep(sleep_time)
total_sleep_time += sleep_time
sleep_time *= 2
else:
# some other error
return None
except Exception as e:
return None
# out of retries
return None
Python os.replace() function
In the library I am using, the set(key, value) method uses the Python os.replace() function.
On Linux it is sufficient to put the os.replace() function in a try-except:
try:
os.replace(src, dst)
return True
except Exception as e:
return False
The function will wait until the file can be replaced. An exception will only be raised if there is a timeout, which on most Linux systems is 60 seconds, or some other unexpected error.
On Windows this will fail immediately if the file is accessed by another thread. To create the same behaviour we have with Linux we must add retries and a delay, for example:
max_sleep_time = 10
total_sleep_time = 0
sleep_time = 0.02
while total_sleep_time < max_sleep_time:
try:
os.replace(src, dst)
return True
except Exception as e:
winerror = getattr(e, 'winerror', None)
if winerror == 5:
time.sleep(sleep_time)
total_sleep_time += sleep_time
sleep_time *= 2
else:
# some other error
return False
# out of retries
return False
Conclusion
Creating a Python program that can run on multiple platforms can be complicated because you can run into problems like the ones described above. At first I was surprised that Python didn't hide the Windows complexity from me. Coming from Linux I thought, Python why don't you make this work on Windows the way it works on Linux?
But that's the choice the Python developers made. It may not be possible either. I couldn't find a single line in the Python docs online that alerted me and noticed that many people struggle with this. I submitted a bug report to ask that a warning be added for developers when developing for multiple platforms. But later refrained from doing so because I realize I am very biased coming from Linux.
Links / credits
backports.py
https://github.com/flennerhag/mlens/blob/master/mlens/externals/joblib/backports.py
os.replace
https://docs.python.org/3/library/os.html?highlight=os%20replace#os.replace
os.replace is not cross-platform: at least improve documentation
https://bugs.python.org/issue46003
Read more
Threads
Recent
- Collect and block IP addresses with ipset and Python
- How to cancel tasks with Python Asynchronous IO (AsyncIO)
- Run a Docker command inside a Docker Cron container
- Creating a Captcha with Flask, WTForms, SQLAlchemy, SQLite
- Multiprocessing, file locking, SQLite and testing
- Sending messages to Slack using chat_postMessage
Most viewed
- Flask RESTful API request parameter validation with Marshmallow schemas
- Using UUIDs instead of Integer Autoincrement Primary Keys with SQLAlchemy and MariaDb
- Using Python's pyOpenSSL to verify SSL certificates downloaded from a host
- Connect to a service on a Docker host from a Docker container
- Using PyInstaller and Cython to create a Python executable
- SQLAlchemy: Using Cascade Deletes to delete related objects