Deploying .NET APIs to Azure App Service is straightforward, all you need do is to create an App Service, push the code, paste a connection string, done. The first time you try the same flow with a Python API — Flask, Django, or FastAPI — three things break in ways that surprise most .NET-first teams: the Windows OS option for App Service Plan for Python might not work properly and you don’t see few options in the App Services.
- ODBC Drivers
- Database Connection String
- Start up Command
In this article, we will unpack all three problems: why Python on Windows App Service is no longer supported (and what to use instead), how the Startup Command works for Python apps, why Python connection strings must specify an ODBC driver name when .NET connection strings never did, and how to install the Microsoft ODBC driver from the Kudu/SSH console of your App Service.
Can Python APIs run on App Service without Docker?
Yes — but only on Linux. Azure App Service ships built-in, fully managed runtime stacks for Python (3.9 through the latest versions) on Linux App Service Plans. You deploy plain code (a zip, Git push, Azure DevOps Pipelines or GitHub Actions), the Oryx build engine detects requirements.txt, creates a virtual environment, installs your dependencies, and runs your app. No Dockerfile, no container registry, no image builds —it is exactly as “code-based” as deploying a .NET app.
Azure portal recommends choosing Linux based App Service Plan for the built-in Python runtime. If you open the portal today and select Python as the runtime stack, the OS radio button locks to Linux — Windows is greyed out as shown in the below screenshot.

Figure 1: When Python is selected as the runtime stack, the portal only allows Linux as the operating system.
Python is not longer supported on Windows App Services. Why did Microsoft drop Python on Windows?
- The Python ecosystem itself is Linux-first. Production servers like Gunicorn do not run on Windows, and many scientific/data packages compile far more reliably on Linux.
- Maintaining security patches for a rarely used Windows Python stack was not sustainable — after the retirement date, Windows Python apps stopped receiving security patches and support.
So what should you choose?
App Service Plan for Linux is the right home for a classic Python web API: one app (or a small number of apps), always-on HTTP workloads, deployment slots, easy custom domains and certificates, and the built-in Python runtime with zero container knowledge required. This is the closest equivalent to the Windows App Service experience your .NET team already knows.
Azure Container Apps is the better fit when your Python API is one microservice among many. It gives you scale-to-zero, KEDA-based event-driven scaling, per-revision traffic splitting, and consumption-based billing. The trade-off: it is container-based, so you (or a build service) produce an image. If your “API” is really a mesh of small services talking to queues and to each other, Container Apps is the more natural platform; if it is a single monolithic API, Linux App Service is simpler and cheaper to operate.
The Startup Command for Python apps
On .NET App Service, you never think about how the process starts — IIS or the .NET hosting model handles it. On Python Linux App Service, the platform starts your app with Gunicorn, a production WSGI server, and it uses auto-detection to figure out what to launch:
That default placeholder page is the classic “my deployment succeeded but I see a sample site” moment. The fix is the Startup Command, found under Settings > Configuration > General settings (Stack settings) in the portal.
Figure 2: The Startup Command field under Stack settings tells App Service exactly how to launch your Python process.
For a FastAPI app whose entry point is app object inside main.py, a typical startup command is:
Python -n unicorn app.main:app –host 0.0.0.0 –port 8000
For anything longer — installing OS packages, running migrations, then starting the server — point the startup command at a shell script deployed with your code (for example startup.sh).
Note: We will use exactly that trick later to solve the ODBC driver problem permanently.
Connection strings: the driver name .NET never asked you for
Here is a typical Azure SQL connection string from a .NET application:
Server=tcp:sql-contoso.database.windows.net,1433;Initial Catalog=SupportDb;
User ID=apiuser;Password=<YOUR-PASSWORD-HERE>;Encrypt=True;
Notice what is missing: any mention of a driver. That is because .NET apps use Microsoft.Data.SqlClient, a fully managed ADO.NET provider that ships inside your application as a NuGet package. The “driver” travels with your code, so the connection string never needs to name it.
Python works differently. The most common SQL Server library, pyodbc, is not a driver — it is a thin wrapper over the ODBC driver manager. The actual driver is a native OS-level component, the Microsoft ODBC Driver for SQL Server, installed separately on the machine. Because one machine can have multiple ODBC drivers installed, your connection string must tell the driver manager exactly which one to load — using the Driver keyword:
import pyodbc
conn_str = (
“Driver={ODBC Driver 18 for SQL Server};”
“Server=tcp:sql-contoso.database.windows.net,1433;”
“Database=SupportDb;”
“Uid=apiuser;”
“Pwd=<YOUR-PASSWORD-HERE>;”
“Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30;”
)
conn = pyodbc.connect(conn_str)
Three details trip people up:
- The braces and exact name matter. {ODBC Driver 18 for SQL Server} must match the installed driver name character for character. If the server only has driver 17 installed and your string says 18, you get the same “no default driver specified” error as having no driver at all.
- Driver 18 changed the encryption default. Starting with ODBC Driver 18, Encrypt defaults to yes. For Azure SQL this is fine (its certificate is trusted), but connections to on-premises servers with self-signed certificates that worked with driver 17 will suddenly fail after upgrading.
- You can ask Python what is installed. pyodbc.drivers() returns the list of registered ODBC drivers — the single most useful diagnostic line when debugging connection issues. An empty list means no driver is installed at all.
>>> import pyodbc
>>> pyodbc.drivers()
[‘ODBC Driver 18 for SQL Server’]
Installing the ODBC driver from the Kudu / SSH console
And that brings us to the surprise waiting inside App Service. Depending on the Python runtime image version, the Microsoft ODBC driver may not be preinstalled in the built-in Linux container. Your code deploys, requirements.txt installs pyodbc happily (it is just the wrapper), and then the first real database call throws:
pyodbc.InterfaceError: (‘IM002’, ‘[IM002] [unixODBC][Driver Manager]
Data source name not found and no default driver specified (0) (SQLDriverConnect)’)
The fix is to install the driver inside the running container. App Service for Linux gives you an SSH session into the app container — either from the portal (Development Tools > SSH) or through the Kudu site at https://<app-name>.scm.azurewebsites.net, which exposes the same SSH/Bash console. The built-in Python images are Debian-based and you run as root inside the container, so apt-get works directly.

Figure 3: The SSH console connects you directly into the running Linux container of your App Service.
Inside the SSH session, run:
# Install the ODBC driver (and unixODBC dev headers)
apt-get update
ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
The catch: it does not survive a restart
Here is the critical caveat. Anything you change outside the /home directory lives in the container filesystem, not in persistent storage. The next time the app restarts, scales, or the platform recycles the container, your manually installed driver is gone and the IM002 error returns. The SSH install is a diagnostic and unblocking technique, not a deployment strategy. For a permanent fix you have two clean options:
- Startup script: put the apt-get commands into a startup.sh deployed with your code, followed by your startup command, in that script. The driver is reinstalled on every container start — these ties together everything in this article.
- Custom container: bake msodbcsql18 into your own image with a Dockerfile and deploy that to App Service or Container Apps. This is the most robust option and the natural choice if you are heading toward the microservices/Container Apps architecture anyway.
A minimal startup.sh looks like this:
#!/bin/bash
ACCEPT_EULA=Y apt-get install -y msodbcsql18
python -m uvicorn app.main:app –host 0.0.0.0 –port 8000
Discover more from Praveen Kumar Sreeram's Blog
Subscribe to get the latest posts sent to your email.