How to Create Interactive Maps in Python with Folium
You may already have GIS data in Python, but static plots are often not enough. In many workflows, you need a map that people can pan, zoom, click, and open in a browser without installing GIS software.
Problem statement
You may already have GIS data in Python, but static plots are often not enough. In many workflows, you need a map that people can pan, zoom, click, and open in a browser without installing GIS software.
This is a common need when you want to:
- show survey points or field assets
- share project boundaries with a client
- display GeoJSON layers interactively
- deliver a lightweight web map as an HTML file
The practical problem is turning point or polygon data into an interactive map quickly, using standard Python tools.
Quick answer
Folium lets you create Leaflet-based interactive maps in Python. You can center a map on coordinates, add markers, load GeoJSON or GeoPandas data, add popups and tooltips, and save the result as an HTML file.
import folium
m = folium.Map(location=[40.7128, -74.0060], zoom_start=12)
folium.Marker(
location=[40.7128, -74.0060],
popup="Project office"
).add_to(m)
m.save("interactive-map.html")
Step-by-step solution
1. Install Folium and supporting libraries
Install Folium and the libraries commonly used to prepare GIS data before mapping:
pip install folium geopandas pandas
Check that Folium is available:
import folium
print(folium.__version__)
If you plan to work with shapefiles or GeoJSON, GeoPandas is usually the easiest way to prepare data before adding it to a map.
2. Create a basic interactive map
Create a minimal map centered on a city:
import folium
# Center on Denver, Colorado
m = folium.Map(location=[39.7392, -104.9903], zoom_start=11)
m.save("denver-map.html")
Open denver-map.html in a browser to view the map.
3. Add markers for point locations
For a single location, add a marker with a popup and tooltip:
import folium
m = folium.Map(location=[39.7392, -104.9903], zoom_start=11)
folium.Marker(
location=[39.7500, -104.9995],
popup="Field office",
tooltip="Click for details"
).add_to(m)
m.save("denver-office-map.html")
For multiple points, loop through your data.
From a Python list
import folium
sites = [
{"name": "Site A", "lat": 39.7392, "lon": -104.9903},
{"name": "Site B", "lat": 39.7295, "lon": -104.9850},
{"name": "Site C", "lat": 39.7498, "lon": -105.0002},
]
m = folium.Map(location=[39.7392, -104.9903], zoom_start=12)
for site in sites:
folium.Marker(
location=[site["lat"], site["lon"]],
popup=site["name"],
icon=folium.Icon(color="blue", icon="info-sign")
).add_to(m)
m.save("site-markers.html")
From a pandas DataFrame
import pandas as pd
import folium
df = pd.DataFrame({
"name": ["Well 1", "Well 2", "Well 3"],
"lat": [35.0844, 35.0944, 35.0744],
"lon": [-106.6504, -106.6404, -106.6604],
"status": ["Active", "Inactive", "Active"]
})
m = folium.Map(location=[35.0844, -106.6504], zoom_start=12)
for _, row in df.iterrows():
folium.Marker(
location=[row["lat"], row["lon"]],
popup=f"{row['name']} ({row['status']})",
tooltip=row["name"],
icon=folium.Icon(color="green" if row["status"] == "Active" else "red")
).add_to(m)
m.save("well-locations.html")
4. Add GeoJSON polygon or line layers
Folium can display GeoJSON directly, which makes it useful for boundaries, service areas, routes, or other vector layers.
import folium
m = folium.Map(location=[37.8, -96], zoom_start=4)
folium.GeoJson(
"counties.geojson",
name="County boundaries",
style_function=lambda feature: {
"fillColor": "#3186cc",
"color": "black",
"weight": 1,
"fillOpacity": 0.3,
},
tooltip=folium.GeoJsonTooltip(fields=["NAME"], aliases=["County:"])
).add_to(m)
folium.LayerControl().add_to(m)
m.save("county-boundaries.html")
This pattern works well when you already have a GeoJSON file and want to style it and show attributes in a tooltip or popup.
5. Use GeoPandas data with Folium
Folium expects web map coordinates in latitude and longitude, usually EPSG:4326. Many GIS files are stored in other coordinate reference systems, so reproject them before display.
import geopandas as gpd
import folium
# Read a shapefile
gdf = gpd.read_file("project_boundaries.shp")
# Reproject to WGS84 for Folium
gdf = gdf.to_crs(epsg=4326)
# Keep only columns needed for interaction
gdf = gdf[["project_id", "name", "geometry"]]
# Create map centered on data using total bounds
minx, miny, maxx, maxy = gdf.total_bounds
center = [(miny + maxy) / 2, (minx + maxx) / 2]
m = folium.Map(location=center, zoom_start=10)
folium.GeoJson(
gdf,
name="Projects",
style_function=lambda feature: {
"fillColor": "#ff7800",
"color": "#333333",
"weight": 2,
"fillOpacity": 0.4,
},
tooltip=folium.GeoJsonTooltip(
fields=["project_id", "name"],
aliases=["Project ID:", "Name:"]
)
).add_to(m)
m.save("project-boundaries.html")
If you want a more reliable center point for polygons, calculate centroids in a projected CRS, then convert back to EPSG:4326 for display. For labeling or guaranteed in-polygon points, use representative_point().
6. Add layer controls and base maps
You can add multiple tile layers and overlays so users can switch basemaps or turn layers on and off.
import folium
m = folium.Map(location=[34.0522, -118.2437], zoom_start=10, tiles="OpenStreetMap")
folium.TileLayer("CartoDB positron", name="Light basemap").add_to(m)
folium.TileLayer("CartoDB dark_matter", name="Dark basemap").add_to(m)
points = folium.FeatureGroup(name="Survey points")
folium.Marker([34.0522, -118.2437], popup="Survey Point 1").add_to(points)
points.add_to(m)
folium.LayerControl().add_to(m)
m.save("layer-control-map.html")
This is useful when you need one output map that supports several layers or display options.
7. Save and share the map
Folium outputs an HTML map file you can open and share easily:
m.save("final-map.html")
You can then:
- open it directly in a browser
- send the HTML file to another user
- include it in internal documentation or reporting workflows
Code examples
Minimal Folium map
import folium
m = folium.Map(location=[51.5074, -0.1278], zoom_start=11)
m.save("london-map.html")
Point map from tabular data
import pandas as pd
import folium
df = pd.DataFrame({
"site": ["North", "Central", "South"],
"lat": [40.78, 40.75, 40.70],
"lon": [-73.96, -73.99, -74.01]
})
m = folium.Map(location=[40.75, -73.98], zoom_start=12)
for _, row in df.iterrows():
folium.Marker(
location=[row["lat"], row["lon"]],
popup=row["site"]
).add_to(m)
m.save("sites-map.html")
GeoPandas to Folium workflow
import geopandas as gpd
import folium
gdf = gpd.read_file("areas.geojson").to_crs(epsg=4326)
minx, miny, maxx, maxy = gdf.total_bounds
center = [(miny + maxy) / 2, (minx + maxx) / 2]
m = folium.Map(location=center, zoom_start=9)
folium.GeoJson(
gdf,
tooltip=folium.GeoJsonTooltip(fields=["name"])
).add_to(m)
m.save("areas-map.html")
Explanation
Folium is a Python interface to Leaflet, a JavaScript library for web maps. Folium handles the HTML and JavaScript generation, so you can build interactive maps from Python without writing frontend code.
In practice, Folium is mainly a display tool. It is best used after you have already done your GIS processing. A common workflow is:
- read and clean data in GeoPandas
- fix or check the CRS
- select the attributes you want to show
- pass the result to Folium for visualization
- export the map to HTML
The most important GIS detail is coordinate reference systems. Web maps use latitude and longitude, so your vector data should usually be in EPSG:4326 before adding it to Folium. If you pass projected coordinates directly, the layer may appear in the wrong location or not render where you expect.
Folium is a good fit when you need:
- a quick interactive map from Python
- a deliverable that opens in a browser
- markers, popups, tooltips, and layer controls
- a simple way to share GIS outputs with non-GIS users
It is not the right tool for heavy spatial analysis, editing, or detailed cartographic control. Use GeoPandas for data preparation and Folium for the final interactive display.
Edge cases or notes
- CRS issues: Reproject GeoDataFrames with
to_crs(epsg=4326)before sending them to Folium. - Latitude and longitude order: Folium uses
[lat, lon], not[lon, lat]. Reversed coordinates are a common cause of misplaced markers. - Invalid geometries: Broken polygons or null geometries can stop layers from rendering correctly. Check with
gdf.is_validand remove or repair invalid features if needed. - Large datasets: Large GeoJSON files can make maps slow. Simplify geometries or reduce feature count before export.
- Shapefiles are not added directly: Read shapefiles with GeoPandas first, then pass the GeoDataFrame to
folium.GeoJson(). - Shared HTML files may still load web resources: In many normal Folium setups, basemaps and some supporting resources are loaded through the browser. Test the output in the environment where it will be used.
Internal links
For a broader overview, see Python for GIS: What It Is and When to Use It.
If you need supporting workflows, read GeoPandas Basics: Working with Spatial Data in Python and Coordinate Reference Systems (CRS) Explained for Python GIS.
If your data appears in the wrong place, start with Coordinate Reference Systems (CRS) Explained for Python GIS.
FAQ
Can Folium display shapefiles directly?
No. Folium does not directly read shapefiles. Use GeoPandas to read the shapefile, reproject it to EPSG:4326, and then pass the GeoDataFrame into folium.GeoJson().
What CRS should I use with Folium?
Use WGS84 latitude and longitude, typically EPSG:4326, when preparing vector data for Folium. CRS problems are one of the most common reasons data appears misplaced.
How do I add popups from my data attributes?
For markers, build the popup string inside a loop:
popup=f"{row['name']} - {row['status']}"
For GeoJSON layers, use folium.GeoJsonTooltip or folium.GeoJsonPopup with field names from your data.
Why is my Folium map blank or misplaced?
Common causes include:
- wrong CRS
- latitude and longitude reversed
- invalid or empty geometries
- data outside the current map extent
- a very large GeoJSON file causing rendering problems
Can I use GeoPandas data directly in Folium?
Yes. After reading and reprojecting the GeoDataFrame, you can pass it directly to folium.GeoJson(gdf) and then add tooltips, styling, and layer controls.
Related articles
Keep exploring with more guides in this category.
Coordinate Reference Systems (CRS) Explained for Python GIS
Understand coordinate reference systems in Python GIS and learn how to check, assign, and convert them with GeoPandas.
Read article →
GeoPandas Basics: Working with Spatial Data in Python
A practical introduction to GeoPandas for reading, inspecting, filtering, and exporting vector spatial data in Python.
Read article →
Python for GIS: What It Is and When to Use It
Learn what Python for GIS means, when to use it, and which tools to start with for automating spatial workflows.
Read article →