Tutorial

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:

  1. read and clean data in GeoPandas
  2. fix or check the CRS
  3. select the attributes you want to show
  4. pass the result to Folium for visualization
  5. 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_valid and 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.

Keep exploring with more guides in this category.