17 Jun 2026

feedDjango community aggregator: Community blog posts

The 2026 way of using importmaps in Django

The 2026 way of using importmaps in Django

I last wrote about Django, JavaScript modules and importmaps in May 2025, slightly over a year ago.

The main topic of this post is the django-js-asset 4.0 release. The library is used in many places, some of the more well-known packages using it are django-mptt and django-ckeditor. I have since done a lot of work evolving the ways of integrating importmaps but the efforts to standardize upon an approach have stalled a bit. The main reason for this, apart from time and energy, was that I wasn't really all that happy with the global importmap. When I had only a few modules using the importmap facility, I didn't care all that much. Now that the recently released django-content-editor 9.0 also uses importmaps for shipping a refactored, much more modular JavaScript implementation while still keeping all the benefits of cache busting using ManifestStaticFilesStorage1, having a global importmap got annoying. The content editor JavaScript is only used within the Django administration interface, but when using a single global importmap object, the importmap entries were always there on each page that used an importmap at all.

A better solution was needed. I'm a big fan of using forms.Media for collecting CSS and JavaScript from widgets, forms and utilities. It helps me avoid inline JavaScript since at least 2017. I'm not using it for site-wide CSS and JavaScript, I'm still transpiling, PostCSS-ing and bundling the assets using rspack as for example written about here and here.

Why importmaps?

A quick refresher on why this matters at all. Django's ManifestStaticFilesStorage hashes the contents of each file into its name for cache busting, but out of the box it doesn't rewrite the import statements inside JavaScript modules. Importmaps bridge the gap: your code imports a stable name:

import { initializeEditors } from "django-prose-editor/editor"

and the importmap tells the browser where that name actually lives:

<script type="importmap">
{"imports": {
  "django-prose-editor/editor": "/static/django_prose_editor/editor.6e8dd4c12e2e.js"
}}
</script>

So the import stays clean and constant while the file behind it can get a new hash on every deploy.

django-js-asset 4.0

The updated django-js-asset 4.0 doesn't ship the old, global importmap at all. This means the upgrade might require some work. Instead of one importmap shared across the whole site, you now get a specific importmap assembled for the context at hand - either by Django itself when it collects the media of your forms, widgets and the admin, or explicitly by you in a view or context processor. The building block in both cases is the ImportMap object; when it travels through js_asset.Media (a subclass of django.forms.Media) the maps are automatically merged into a single <script type="importmap">, by customizing and extending what Django does already when merging media instances.

The release notes go into more detail.

In practice

If you're using a package such as django-prose-editor in the Django admin you don't have to do anything, things should just work.

If you're using such a package outside the admin, you have to remove "js_asset.context_processors.importmap" from your list of context processors. On one particular website the prose editor is the only package with importmap entries outside the admin, so I have to add the importmap to the template context myself:

from django_prose_editor.widgets import importmap

def view(request, ...):
    return render(request, "template.html", {
        # ...
        "importmap": importmap,
    })

The template then just renders it in the <head>:

... {{ importmap }}</head>

On a different site, I have a slightly more involved scenario where I previously used importmap.update(...) to add my own entries to the importmap. There, I'm using a custom context processor to always add these entries to the importmap too:

from django_prose_editor.widgets import importmap as dpe_importmap
from js_asset import ImportMap, static_lazy

_site_importmap = ImportMap({
    "imports": {
        "my-module": static_lazy("my-module.js"),
    }
})
_importmap = dpe_importmap | _site_importmap

def importmap(request):
    return {"importmap": _importmap}

This importmap is merged once at server startup and then served repeatedly to the client. Because we use the lazy version of the static function we can do this during startup and not worry about files not yet collected by collectstatic - we'll get the correct paths later.

On the same site as the previous example, I also have an admin inline which requires some JavaScript and also an importmap:

from django.contrib import admin
from django.forms import Script
from js_asset import Media, ImportMap

# Initializing this once. Not necessary but I like it better that way.
_importmap = ImportMap({
    "imports": {
        # ...
    }
})

class ModelInline(admin.StackedInline):
    @property
    def media(self):
        return Media(
            js=[
                _importmap,
                Script("module.js", type="module"),
            ]
        )

As of 4.0, JS and CSS produce Django's own Script and Stylesheet objects, so you can import and use Script directly from django.forms as shown above (on Django 4.2-5.1, import it from js_asset instead, which backports it). The familiar JS("module.js", {"type": "module"}) wrapper still works unchanged if you prefer it - it just takes a positional dict instead of keyword arguments.

Here, it's really important to use the js_asset.Media and not django.forms.Media. js_asset.Media knows how to handle importmaps - all importmaps are collected from all media lists, merged and added to the output before all other CSS and especially JavaScript. The reason for that is that browsers only honour a single importmap per page, and it really has to appear before all JavaScript modules referencing any entries in the importmap.

The nice thing about js_asset.Media is that it doesn't have to appear first in the list of media classes which are merged - it can also appear in the middle or last, and still can do its magic after all Media objects have been merged into a single one.

The rest is handled by Django itself, since it already supports collecting media assets. The missing piece was just the importmap object and the js_asset.Media class which knows how to special case them, and which - through the power of overriding __add__ and __radd__ takes over all the other media instances.

What's next

I haven't yet used CSP nonces using {% csp_nonce_attr media %} in production myself, but it should just work, even with importmaps and everything else. Given that I have a passing test suite I have no reason to believe it doesn't already work, but I'd like to have a confirmation.

I'm hoping to standardize some more. If we could get something like this in Django core that would be really nice. Maybe I'll be able to work on that at Django on the Med 🏖️. Since no browser supports multiple importmaps as of today having multiple implementations of importmaps in the Django ecosystem will lead to trouble down the road. I think there is a clear case to be made for importmap support in Django and I would obviously love it if the approach implemented today in django-js-asset would be the basis for the official solution.


  1. Without having to do any overrides to enable ESM support.

17 Jun 2026 5:00pm GMT

feedPlanet Python

PyCharm

Every developer has tools they rely on daily. The workflows they've built around them, the ways they've learned to move faster, debug smarter, and write better code - that kind of hands-on experience can be hard to put into words.

We're collaborating with LinkedIn to make it easier for you to showcase your expertise with JetBrains IDEs on the world's largest professional network. You can now connect your IDE to LinkedIn and let your real tool usage speak for itself.

Connect your IDE

IntelliJ IDEA, PyCharm, WebStorm, PhpStorm, Rider, GoLand, CLion, RustRover, and RubyMine are already supported via a free plugin, while support for DataGrip is coming soon.

In this blog post, we'll explain what LinkedIn connected apps are, what they mean for your profile, and how to get started.

What this is about

Building on early collaboration with Descript, Duolingo, Lovable, Relay.app, and Replit, LinkedIn is expanding the range of apps you can feature on your LinkedIn profile, turning real-world product usage into a credible, visible signal of practical tool experience. We're glad to join forces with them to bring this to JetBrains IDE users.

"We're building new ways for members to show real, credible proof of what they're capable of, right on their LinkedIn profile. And for the brands behind these tools, there's no better endorsement than a customer who's actively using and loving your product."
- Dan Shapero, CEO of LinkedIn

Connected apps let you link the tools you use in your daily work directly to your LinkedIn profile, where they appear prominently, helping you stand out to your professional network. Once connected, each app generates a simple statement based on how you actually use it. Unlike manually added skills, this is based on real usage and updates automatically as your experience evolves.

How to get started

Open your JetBrains IDE, go to Settings | Plugins, search for the LinkedIn Connected Apps plugin under the Marketplace tab, and install it.

Once installed, the plugin starts collecting data locally about how you use your IDE. Depending on your usage history, you may receive an initial statement right away, which will then be updated once the plugin has collected enough data to better reflect your real IDE expertise.

LinkedIn-integration-in-JetBrains-IDEs-example

Your IDE usage data stays on your machine. When you are ready, you can connect your LinkedIn account and share your IDE expertise badge there. If you keep the plugin installed, your badge will update automatically as your IDE usage evolves.

The plugin is free for all JetBrains IDE users.

What's included in this release

This is the first version of the integration, delivered as a standalone plugin rather than being built directly into the IDE. It covers IntelliJ IDEA, PyCharm, WebStorm, GoLand, PhpStorm, Rider, CLion, RustRover, and RubyMine; DataGrip is not yet supported.

Usage is detected within the IDE itself, so if you use AI features via an external tool or terminal, those won't be reflected yet.

How your IDE expertise is determined

The model is intentionally simple for now. It is designed to represent your practical use of JetBrains tools, not to rank developers or certify skill levels. Our goal was to provide a solid starting point, but we know there's more to capture about how developers work with their IDEs.

Statements map to different levels of experience and are generated based on how you interact with your IDE - from writing and editing code using basic features to working with debugging tools, version control, and AI-assisted workflows. For more information, see our documentation.

What's coming next

We're already working on the next version, planned for later this year. We'll focus on improving how IDE usage, including AI feature usage, is represented, expanding support to DataGrip, and making the overall experience feel more integrated.

FAQ

Which JetBrains IDEs are supported?

IntelliJ IDEA, PyCharm, WebStorm, GoLand, PhpStorm, Rider, CLion, RustRover, and RubyMine. Support for DataGrip is coming soon.

Why isn't DataGrip supported yet?

DataGrip is designed for working with databases and includes workflows that differ from our other IDEs. We plan to support it soon.

Can I connect multiple IDEs?

Yes, if you use multiple supported JetBrains IDEs, you can connect each of them. You'll get a badge for all connected IDEs.

Note: If you use multiple JetBrains accounts across different IDE instances but link them all to the same LinkedIn profile, IDE usage statements from each account will be displayed on that LinkedIn profile.

Do I need to keep the plugin installed after connecting?

You can share your IDE usage statement once and then remove the plugin, but note that it must remain installed if you want to track your ongoing progress and have any changes reflected on LinkedIn.

Is this feature free?

Yes, it's available to all JetBrains IDE users at no cost.

Is this a certification?

Connected apps reflect real IDE usage and are designed to showcase applied experience, not to act as a formal certification or skill ranking. Certifications, degrees, and licenses remain important markers of professional achievement. Connected apps on LinkedIn add a different kind of signal: partner-validated tool usage that reflects practical work and can update over time.

What data is shared with LinkedIn and JetBrains?

Only the information required to represent your connected account and IDE usage statement.

Will this help me get hired?

Having connected apps on your LinkedIn profile is extra proof of your practical experience with leading tools. While connected apps make your expertise visible, they are just one part of your profile. Think of it as a way to let your tooling speak for itself.

Give it a try and let us know what you think in the comments below. We're continuing to develop this integration, and your feedback will help shape what comes next.

17 Jun 2026 4:40pm GMT

Talk Python to Me: #552: Astral joins OpenAI

OpenAI just acquired Astral, the company behind uv, Ruff, and ty. And if your first thought was "wait, is uv toast?", you are not alone. But here's the twist Charlie Marsh shared with me: he thinks they may ship more open source at OpenAI than they ever did at Astral. On this episode, we get into the acquisition, the mixed feelings, the future of your favorite Python tools, and what it's like to build right at the center of the AI universe.<br/> <br/> <strong>Episode sponsors</strong><br/> <br/> <a href='https://talkpython.fm/sentry'>Sentry Error Monitoring, Code talkpython26</a><br> <a href='https://talkpython.fm/training'>Talk Python Courses</a><br/> <br/> <h2 class="links-heading mb-4">Links from the show</h2> <div><strong>Guest</strong><br/> <strong>Charlie Marsh</strong>: <a href="https://github.com/charliermarsh?featured_on=talkpython" target="_blank" >github.com</a><br/> <br/> <strong>The announcement</strong>: <a href="https://astral.sh/blog/openai?featured_on=talkpython" target="_blank" >astral.sh</a><br/> <strong>OpenAI</strong>: <a href="https://openai.com/?featured_on=talkpython" target="_blank" >openai.com</a><br/> <strong>uv</strong>: <a href="https://github.com/astral-sh/uv?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>ty</strong>: <a href="https://github.com/astral-sh/ty?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>Ruff</strong>: <a href="https://github.com/astral-sh/ruff?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>pyx</strong>: <a href="https://astral.sh/pyx?featured_on=talkpython" target="_blank" >astral.sh</a><br/> <strong>Codex team</strong>: <a href="https://openai.com/codex/?featured_on=talkpython" target="_blank" >openai.com</a><br/> <strong>Anthropic did something similar by acquiring Bun</strong>: <a href="https://www.anthropic.com/news/anthropic-acquires-bun-as-claude-code-reaches-usd1b-milestone?featured_on=talkpython" target="_blank" >www.anthropic.com</a><br/> <strong>Daily Stars Explorer</strong>: <a href="https://emanuelef.github.io/daily-stars-explorer/#/astral-sh/uv" target="_blank" >emanuelef.github.io</a><br/> <br/> <strong>Agentic AI Programming for Python</strong>: <a href="https://training.talkpython.fm/courses/agentic-ai-programming-for-python" target="_blank" >training.talkpython.fm</a><br/> <strong>Python Web Security: OWASP Top 10 with Agentic AI</strong>: <a href="https://training.talkpython.fm/courses/agentic-ai-python-security" target="_blank" >training.talkpython.fm</a><br/> <br/> <strong>Episode #552 deep-dive</strong>: <a href="https://talkpython.fm/episodes/show/552/astral-joins-openai#takeaways-anchor" target="_blank" >talkpython.fm/552</a><br/> <strong>Episode transcripts</strong>: <a href="https://talkpython.fm/episodes/transcript/552/astral-joins-openai" target="_blank" >talkpython.fm</a><br/> <br/> <strong>Theme Song: Developer Rap</strong><br/> <strong>🥁 Served in a Flask 🎸</strong>: <a href="https://talkpython.fm/flasksong" target="_blank" >talkpython.fm/flasksong</a><br/> <br/> <strong>---== Don't be a stranger ==---</strong><br/> <strong>YouTube</strong>: <a href="https://talkpython.fm/youtube" target="_blank" ><i class="fa-brands fa-youtube"></i> youtube.com/@talkpython</a><br/> <br/> <strong>Bluesky</strong>: <a href="https://bsky.app/profile/talkpython.fm" target="_blank" >@talkpython.fm</a><br/> <strong>Mastodon</strong>: <a href="https://fosstodon.org/web/@talkpython" target="_blank" ><i class="fa-brands fa-mastodon"></i> @talkpython@fosstodon.org</a><br/> <strong>X.com</strong>: <a href="https://x.com/talkpython" target="_blank" ><i class="fa-brands fa-twitter"></i> @talkpython</a><br/> <br/> <strong>Michael on Bluesky</strong>: <a href="https://bsky.app/profile/mkennedy.codes?featured_on=talkpython" target="_blank" >@mkennedy.codes</a><br/> <strong>Michael on Mastodon</strong>: <a href="https://fosstodon.org/web/@mkennedy" target="_blank" ><i class="fa-brands fa-mastodon"></i> @mkennedy@fosstodon.org</a><br/> <strong>Michael on X.com</strong>: <a href="https://x.com/mkennedy?featured_on=talkpython" target="_blank" ><i class="fa-brands fa-twitter"></i> @mkennedy</a><br/></div>

17 Jun 2026 3:20pm GMT

PyCharm: Your JetBrains IDE Expertise, Now on LinkedIn

17 Jun 2026 1:11pm GMT

16 Jun 2026

feedDjango community aggregator: Community blog posts

Cheating as a programming discipline

Great programmers cheat. A hard problem gets quietly swapped for an easier one; a transaction-grade database is replaced by a flat file nobody misses; machinery everyone else considers mandatory simply never gets built. They know a lot - and that's exactly why they get away with it.

Cheating as a programming discipline

16 Jun 2026 11:00am GMT

15 Jun 2026

feedDjango community aggregator: Community blog posts

LLM Inspired Development

How Claude inadvertently suggested new features for my personal site.

15 Jun 2026 3:28pm GMT

09 Jun 2026

feedPlanet Twisted

Hynek Schlawack: How to Ditch Codecov for Python Projects

Codecov's unreliability breaking CI on my open source projects has been a constant source of frustration for me for years. I have found a way to enforce coverage over a whole GitHub Actions build matrix that doesn't rely on third-party services.

09 Jun 2026 12:00am GMT

22 May 2026

feedPlanet Twisted

Glyph Lefkowitz: Opaque Types in Python

Let's say you're writing a Python library.

In this library, you have some collection of state that represents "options" or "configuration" for a bunch of operations. Such a set of options is a bundle of potentially ever-increasing complexity. Thus, you will want it to have an extremely minimal compatibility surface, with a very carefully chosen public interface, that is either small, or perhaps nothing at all. Such an object conveys state and might have some private behavior, but all you want consumers to be able to do is build it in very constrained, specific ways, and then pass it along as a parameter to your own APIs.

By way of example, imagine that you're wrapping a library that handles shipping physical packages.

There are a zillion ways to do it ship a package. There are different carriers who can ship it for you. There's air freight, and ground freight, and sea freight. There's overnight shipping. There's the option to require a signature. There's package tracking and certified mail. Suffice it to say, lots of stuff.

If you are starting out to implement such a library, you might need an object called something like ShippingOptions that encapsulates some of this. At the core of your library you might have a function like this:

1
2
3
4
5
async def shipPackage(
        how: ShippingOptions,
        where: Address,
    ) -> ShippingStatus:
    ...

If you are starting out implementing such a library, you know that you're going to get the initial implementation of ShippingOptions wrong; or, at the very least, if not "wrong", then "incomplete". You should not want to commit to an expansive public API with a ton of different attributes until you really understand the problem domain pretty well.

Yet, ShippingOptions is absolutely vital to the rest of your library. You'll need to construct it and pass it to various methods like estimateShippingCost and shipPackage. So you're not going to want a ton of complexity and churn as you evolve it to be more complex.

Worse yet, this object has to hold a ton of state. It's got attributes, maybe even quite complex internal attributes that relate to different shipping services.

Right now, today, you need to add something so you can have "no rush", "standard" and "expedited" options. You can't just put off implementing that indefinitely until you can come up with the perfect shape. What to do?

The tool you want here is the opaque data type design pattern. C is lousy with such things (FILE, pthread_*_t, fd_set, etc). A typedef in a header file can easily achieve this.

But in Python, if you expose a dataclass - or any class, really - even if you keep all your fields private, the constructor is still, inherently, public. You can make it raise an exception or something, but your type checker still won't help your users; it'll still look like it's a normal class.

Luckily, Python typing provides a tool for this: typing.NewType.

Let's review our requirements:

  1. We need a type that our client code can use in its type annotations; it needs to be public.
  2. They need to be able to consruct it somehow, even if they shouldn't be able to see its attributes or its internal constructor arguments.
  3. To express high-level things (like "ship fast") that should stay supported as we add more nuanced and complex configurations in the future (like "ship with the fastest possible option provided by the lowest-cost carrier that supports signature verification").

In order to solve these problems respectively, we will use:

  1. a public NewType, which gives us our public name...
  2. which wraps a private class with entirely private attributes, to give us an actual data structure, while not exposing the constructor,
  3. a set of public constructor functions, which returns our NewType.

When we put that all together, it looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from dataclasses import dataclass
from typing import Literal, NewType

@dataclass
class _RealShipOpts:
    _speed: Literal["fast", "normal", "slow"]

ShippingOptions = NewType("ShippingOptions", _RealShipOpts)

def shipFast() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts("fast"))

def shipNormal() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts("normal"))

def shipSlow() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts("slow"))

As a snapshot in time, this is not all that interesting; we could have just exposed _RealShipOpts as a public class and saved ourselves some time. The fact that this exposes a constructor that takes a string is not a big deal for the present moment. For an initial quick and dirty implementation, we can just do checks like if options._speed == "fast" in our shipping and estimation code.

However, the main thing we are doing here is preserving our flexibility to evolve the related APIs into the future, so let's see how we might do that. For example, let's allow the shipping options to contain a concrete and specific carrier and freight method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from dataclasses import dataclass
from enum import Enum, auto
from typing import NewType

class Carrier(Enum):
    FedEx = auto()
    USPS = auto()
    DHL = auto()
    UPS = auto()

class Conveyance(Enum):
    air = auto()
    truck = auto()
    train = auto()

@dataclass
class _RealShipOpts:
    _carrier: Carrier
    _freight: Conveyance

ShippingOptions = NewType("ShippingOptions", _RealShipOpts)

def shipFast() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts(Carrier.FedEx, Conveyance.air))

def shipNormal() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts(Carrier.UPS, Conveyance.truck))

def shipSlow() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts(Carrier.USPS, Conveyance.train))

def shippingDetailed(
    carrier: Carrier, conveyance: Conveyance
) -> ShippingOptions:
    return ShippingOptions(_RealShipOpts(carrier, conveyance))

As a NewType, our public ShippingOptions type doesn't have a constructor. Since _RealShipOpts is private, and all its attributes are private, we can completely remove the old versions.

Anything within our shipping library can still access the private variables on ShippingOptions; as a NewType, it's the same type as its base at runtime, so it presents minimal1 overhead.

Clients outside our shipping library can still call all of our public constructors: shipFast, shipNormal, and shipSlow all still work with the same (as far as calling code knows) signature and behavior.

If you need to build and convey some state within your public API, while avoiding breakages associated with compatibility churn, hopefully this technique can help you do that!


Acknowledgments

Thanks for reading, and thank you to my patrons who are supporting my writing on this blog. If you like what you've read here and you'd like to read more of it, or you'd like to support my various open-source endeavors, you can support my work as a sponsor.


  1. The overhead is minimal, but it is not completely zero. The suggested idiom for converting to a NewType is to call it like a function, as I've done in these examples, but if you are wanting to use this pattern inside of a hot loop, you can use # type: ignore[return-value] comments to avoid that small cost.

22 May 2026 12:33am GMT

04 Apr 2026

feedPlanet Twisted

Donovan Preston: Using osascript with terminal agents on macOS

Here is a useful trick that is unreasonably effective for simple computer use goals using modern terminal agents. On macOS, there has been a terminal osascript command since the original release of Mac OS X. All you have to do is suggest your agent use it and it can perform any application control action available in any AppleScript dictionary for any Mac app. No MCP set up or tools required at all. Agents are much more adapt at using rod terminal commands, especially ones that haven't changed in 30 years. Having a computer control interface that hasn't changed in 30 years and has extensive examples in the Internet corpus makes modern models understand how to use these tools basically Effortlessly. macOS locks down these permissions pretty heavily nowadays though, so you will have to grant the application control permission to terminal. But once you have done that, the range of possibilities for commanding applications using natural language is quite extensive. Also, for both Safari and chrome on Mac, you are going to want to turn on JavaScript over AppleScript permission. This basically allows claude or another agent to debug your web applications live for you as you are using them.In chrome, go to the view menu, developer submenu, and choose "Allow JavaScript from Apple events". In Safari, it's under the safari menu, settings, developer, "Allow JavaScript from Apple events". Then you can do something like "Hey Claude, would you Please use osascript to navigate the front chrome tab to hacker news". Once you suggest using OSA script in a session it will figure out pretty quickly what it can do with it. Of course you can ask it to do casual things like open your mail app or whatever. Then you can figure out what other things will work like please click around my web app or check the JavaScript Console for errors. Another very important tips for using modern agents is to try to practice using speech to text. I think speaking might be something like five times faster than typing. It takes a lot of time to get used to, especially after a lifetime of programming by typing, but it's a very interesting and a different experience and once you have a lot of practice It starts to to feel effortless.

04 Apr 2026 1:31pm GMT