Speaker, author, and software developer recognized with a Microsoft MVP Award for Developer Technologies. Stacy has been developing solutions since the mid-1990s across various industries. She authored Beginning Azure Static Web Apps and is passionate about Azure, DevOps, and empowering developers.
You can follow Stacy on Social Media
https://x.com/Stacy_Cash
https://www.stacy-clouds.net
https://nl.linkedin.com/in/stacycash
https://tech.lgbt/@stacyclouds
https://bsky.app/profile/stacy-clouds.net
PLEASE SUBSCRIBE TO THE PODCAST
- Spotify: http://isaacl.dev/podcast-spotify
- Apple Podcasts: http://isaacl.dev/podcast-apple
- Google Podcasts: http://isaacl.dev/podcast-google
- RSS: http://isaacl.dev/podcast-rss
You can check out more episodes of Coffee and Open Source on https://www.coffeeandopensource.com
Coffee and Open Source is hosted by Isaac Levin (https://twitter.com/isaacrlevin)

Apple design kits for Figma and Sketch are now available for iOS, iPadOS, and macOS 27. These include:
Learn about natural language processing and explore the Hugging Face Transformers library in Python for tasks like text classification and summarization.
Natural Language Processing (NLP) focuses on enabling computers to understand, analyze and generate human language.
Modern NLP systems are largely powered by transformer architectures, which introduced the self-attention mechanism to model relationships between words regardless of their position in a sentence. This approach significantly improved performance on tasks such as text classification, summarization, translation and question answering, while also enabling models to capture long-range context more efficiently than earlier methods, such as recurrent neural networks (RNNs).
Popular transformer models include:
These models are pretrained on massive text datasets and can be fine-tuned for specific NLP tasks.
Hugging Face has become one of the most popular platforms for working with these models, providing easy-to-use tools, pretrained transformer models and Python libraries that allow developers to build powerful NLP applications with minimal code.
In this article, we will explore the basics of NLP using the Hugging Face Transformers library in Python, demonstrating how to implement common tasks such as text classification and text summarization using modern transformer models.
The Hugging Face Transformers library includes thousands of pretrained models that can be easily downloaded and used with just a few lines of Python code.
Key components of the Hugging Face ecosystem include:
The library supports both PyTorch and TensorFlow, making it flexible for different deep learning workflows.
In this tutorial, I assume you are new to Python. First, verify that Python is installed on your system. Then run the following commands to check the Python version and create or activate a virtual environment before installing the required dependencies.
python3 --versionpython3 -m venv .venvsource .venv/bin/activateAfter that, run the command to install transformers in the project.
pip3 install transformers torchAfter that, add requirement.txt file in your project, and add entry:
pandas>=2.0.0
And then again run the command pip3 install -r requirements.txt.
At this point, you have installed all the required dependencies and set up the Python project environment. Now we can start building some NLP pipelines using the libraries we configured.
Text classification is one of the most common NLP tasks. It involves assigning predefined categories to text. Examples include sentiment analysis, spam detection and topic classification.
Using Hugging Face, we can create a text classification model in just a few lines of Python, as shown below.
from transformers import pipeline
import pandas as pd
classifier = pipeline("text-classification")
output = classifier("I am excited for final cricket match")
df = pd.DataFrame(output)
print(df)
You should get the output shown below.

In this example, the pipeline automatically loads a pretrained transformer model that performs sentiment analysis. The model analyzes the text and predicts whether the sentiment is positive or negative.
The pipeline is a helper function, that performs three tasks.

Essentially it takes a raw input and gives you clean output.
Using the pipeline wrapper, it is very easy to work with Named Entity Recognition (NER).
from transformers import pipeline
import pandas as pd
tags = pipeline("ner",aggregation_strategy="simple")
customer_feedback = "Barack Obama visited Tokyo in April 2014 and met with Shinzo Abe. The two leaders discussed trade agreements between the United States and Japan. Later, Obama gave a speech at the Roppongi Hills complex. The New York Times covered the event."
output = tags(customer_feedback)
df = pd.DataFrame(output)
print(df)
We are using Hugging Face NER pipeline to process feedback text, automatically identify named entities (such as people, locations and organizations). After converting the extracted entities into a tabular format using the pandas data frame=, you should get this output:

In the example above, we use the NER pipeline to identify named entities in text, such as people, places and organizations.
The aggregation_strategy="simple" option is used to merge subword tokens back into complete entities. Without this setting, some models may split words into subword pieces (for example, “Obama” may appear as “Ob” and “##ama”). Using the “simple” aggregation strategy keeps these fragments combined and returned as a single entity span (such as “Obama”).
Let’s try text summarization without using pipeline. We can do that in three steps as shown below:
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
model_name = "facebook/bart-large-cnn"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
text = """
your text
"""
inputs = tokenizer(
text,
return_tensors="pt",
max_length=1024,
truncation=True,
padding=True,
)
outputs = model.generate(
**inputs,
max_length=150,
min_length=30,
num_beams=4,
early_stopping=True,
)
summary = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(summary)
The example above follows these steps:
We are following all steps of preprocessing, running the model and the postprocessing as we are not using pipeline. This should give you an idea about pipeline utility function.
So, transformers are particularly powerful for NLP because they process text in parallel and capture context efficiently. The self-attention mechanism allows the model to determine which words in a sentence are most relevant when interpreting meaning.
For example, in the sentence:
“Apple released a new product.”
The word Apple could refer to a fruit or a technology company. A transformer model can use surrounding words like released and product to infer that the sentence refers to the company.
This contextual understanding is one of the major breakthroughs that transformer models introduced.
Transformer-based NLP systems are now used in many real-world applications:
Companies increasingly rely on transformer models to analyze large volumes of text data and automate tasks that previously required human interpretation.
I hope that you now have a solid understanding of transformers. Thanks for reading this article.
Learn how to handle mutations with TanStack Query in Vue to be able to send data back the server and keep the whole app in sync.
In my previous article on Data Fetching with TanStack Query for Vue, we went through all the things TanStack Query gives us for free when it comes to pulling data from an API: caching, automatic refetching on window focus, stale times, polling and a much cleaner template than the usual ref plus onMounted dance.
But of course, fetching data is only half of the story. At some point you’re going to need to actually send data back to the server. Creating users, updating records, deleting items, triggering actions. That’s where mutations come in!
In this article we’ll take a look at how to handle mutations with TanStack Query in Vue, how to hook into their lifecycle, how to keep the rest of your app in sync after something changes, and a little bit on optimistic updates.
In the context of TanStack Query, a mutation is any operation that changes server state. POST, PUT, PATCH, DELETE, basically anything that isn’t a GET. Queries fetch data; mutations change it.
The reason TanStack Query treats mutations as their own concept instead of just “another query” is because they behave very differently:
We’ll address all of these, let’s start with the basics.
The composable we’re going to use is useMutation, and the API is refreshingly simple. Let’s build a form that creates a new user.
CreateUser.vue
<script setup>
import { ref } from 'vue'
import { useMutation } from '@tanstack/vue-query'
const name = ref('')
const email = ref('')
const createUser = async (payload) => {
const response = await fetch('https://myapp.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
if (!response.ok) {
throw new Error('Failed to create user')
}
return response.json()
}
const { mutate, isPending, isError, error } = useMutation({
mutationFn: createUser,
})
const onSubmit = () => {
mutate({ name: name.value, email: email.value })
}
</script>
<template>
<form @submit.prevent="onSubmit">
<input v-model="name" placeholder="Name" />
<input v-model="email" placeholder="Email" />
<button :disabled="isPending" type="submit">
{{ isPending ? 'Saving...' : 'Create user' }}
</button>
<p v-if="isError">Something went wrong: {{ error.message }}</p>
</form>
</template>
Let’s break it down. useMutation takes a configuration object with one essential property:
mutationFn: The async function that actually performs the mutation. It must return a promise. If the promise rejects, TanStack Query considers the mutation failed.Notice that unlike useQuery, there’s no mutationKey required here. Mutations can be given a key for advanced scenarios, but for most applications you’ll never need one.
The composable returns several reactive properties, and the ones you’ll end up using the most are:
mutate: The function you call to actually trigger the mutation. Any arguments you pass here are forwarded as the first parameter of your mutationFn.isPending: A boolean that is true while the mutation is in flight. This one used to be called isLoading in older versions, so if you come across old examples online, that’s why.isError / isSuccess: Booleans that reflect the final state of the last attempt.error: Whatever error was thrown from the mutationFn if it failed.data: The resolved value from the mutationFn if it succeeded.I can’t stress enough how much cleaner this ends up being compared to writing your own try/catch plus a loading ref plus an error ref every single time you need a form submission.
useMutation actually returns two different functions that you can use to trigger the mutation: mutate and mutateAsync. They do the same thing but in different ways.
mutate(variables) is fire and forget. It does not return a promise. Errors are swallowed by TanStack Query and surfaced through the reactive isError and error properties. You cannot await it.mutateAsync(variables) returns a promise that you can await. If the mutation fails, the promise rejects, and you’re expected to handle the error yourself with a try/catch.I normally use mutate most of the time because the reactive state is usually all I need. mutateAsync is really nice when you need to chain multiple operations together or when you’re inside a larger async flow and you want the mutation to behave like any other awaited call.
const { mutateAsync } = useMutation({ mutationFn: createUser })
const onSubmit = async () => {
try {
const user = await mutateAsync({ name: name.value })
router.push(`/users/${user.id}`)
} catch (err) {
console.error('Could not create user', err)
}
}
A word of caution: if you use mutateAsync and forget the try/catch, you’ll get an unhandled promise rejection warning in your console.
Let’s dive into the juicy bits of mutations now.
One of the things that makes useMutation so useful in real applications is the set of lifecycle callbacks it exposes. You can pass them either when you create the mutation or when you call mutate.
const { mutate } = useMutation({
mutationFn: createUser,
onSuccess: (data, variables) => {
// data is what mutationFn returned
console.log('User created!', data)
},
onError: (error, variables) => {
console.error('Something broke', error)
},
onSettled: (data, error, variables) => {
// Runs no matter what, after onSuccess or onError
},
})
The three callbacks you’ll use the most are:
onSuccess: Fires when the mutation resolves successfully. Perfect for showing a toast, closing a modal, resetting a form or navigating away.onError: Fires when the mutation rejects. Great for surfacing error messages to the user.onSettled: Fires after either onSuccess or onError. Ideal for cleanup logic that needs to run regardless of outcome.Keep in mind that you can also pass these callbacks directly to the mutate:
mutate(payload, {
onSuccess: () => {
showToast('Saved!')
closeModal()
},
})
If you define callbacks in both places, both will run. The ones on useMutation fire first, then the ones passed to mutate. Remember this if you end up seeing duplicate toasts or double navigations!
Normally you would use the mutate call callbacks whenever you need a specific call of the mutation to do something additional to what is already defined on the useMutation call.
Here’s where mutations really start to shine, and where most of the real world value of TanStack Query lives.
Picture this scenario. You have a user list page that uses useQuery with the key ['users']. The user clicks a button to create a new user, the mutation fires, the backend responds happily with a 201, and… queue drumroll, the list on the screen still shows the old data. The cache has no idea anything changed.
The fix is to tell TanStack Query that the ['users'] query is now stale and should be refetched. We do that via the query client’s invalidateQueries method.
import { useMutation, useQueryClient } from '@tanstack/vue-query'
const queryClient = useQueryClient()
const { mutate } = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
Any query with a matching key is marked stale and, if it is being observed by a mounted component, refetched immediately. If no component is currently observing it, it will simply be refetched the next time it gets mounted.
Pay close attention to what you set to the queryKey!
invalidateQueries({ queryKey: ['users'] }) will also invalidate ['users', 1], ['users', 2] and so on. This default behavior is usually what you will want for you applications, and you will end up setting up keys with specific words like users that you want to invalidate in groups.
If you only want to invalidate exact matches, you can pass exact: true.
queryClient.invalidateQueries({ queryKey: ['users'], exact: true })
I’ve found that 95% of the mutations I write end up following the exact same shape: do the thing, then invalidate the related list and/or detail queries. Once this clicks for you, you’ll start thinking of your data in terms of which queries each mutation affects.
Sometimes waiting for the server feels sluggish. A user clicks “like” on a post, and the heart icon just sits there for 300ms while the request round trips. The fix for this is optimistic updates: update the UI as if the mutation already succeeded, then reconcile with the server afterward.
TanStack Query has first class support for this through the onMutate callback. The general idea goes like this:
onMutate, cancel any in flight queries for the related key, grab a snapshot of the current cached data, and write the optimistic value into the cache.onMutate so you can use it later.onError, roll back to the snapshot.onSettled, invalidate the query to make sure the real server data eventually wins.const likePost = () => { someApiCall() }
const queryClient = useQueryClient()
const { mutate } = useMutation({
mutationFn: likePost,
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ['posts'] })
const previousPosts = queryClient.getQueryData(['posts'])
// We find the postId and add +1 the likes in the query data
queryClient.setQueryData(['posts'], (old) =>
old.map((p) => p.id === postId ? { ...p, likes: p.likes + 1 } : p)
)
// Don't forget to retun the modified data!
return { previousPosts }
},
onError: (err, variables, context) => {
// The `context` here is what we returned in onMutate
queryClient.setQueryData(['posts'], context.previousPosts)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})
To be quite honest with you, optimistic updates are one of those features that sound wonderful on paper but that I only reach for when the UX genuinely demands it.
For the vast majority of forms and buttons in a typical CRUD app, the plain old “show a spinner, invalidate on success” flow is perfectly fine and a lot easier to reason about. Reach for optimistic updates when the snappiness actually matters, not by default.
Mutations are the missing half of the TanStack Query story, and once you pair them with invalidateQueries, the library really starts to pay for itself. You get a consistent way to handle every form, every button, every “do something on the server” action across your entire app. No more scattered loading refs, no more forgetting to refetch data after an update.
Today we covered the basics of useMutation, the difference between mutate and mutateAsync, the lifecycle callbacks, query invalidation and a quick look at optimistic updates. From here I’d recommend poking around the official mutations guide to see the more advanced patterns like mutation scopes and retry configuration.
Happy mutating!