Chaining list operations in Python I assigned the following contrived problem in a comparative programming languages course to give students practice with &quot;streaming&quot;:

Write function that returns the top ten players by points-per-game among the players that have been in 15 games or more. The input to your function will be an object, keyed by team, with a list of player stats. Each player stat is an array with the player name, the number of games played, and the total number of points.

A sample data set is as follows:
stats = {
    'ATL': [
        ['Betnijah Laney', 16, 263],
        ['Courtney Williams', 14, 193],
    ],
    'CHI': [
        ['Kahleah Copper', 17, 267],
        ['Allie Quigley', 17, 260],
        ['Courtney Vandersloot', 17, 225],
    ],
    'CONN': [
        ['DeWanna Bonner', 16, 285],
        ['Alyssa Thomas', 16, 241],
    ],
    'DAL': [
        ['Arike Ogunbowale', 16, 352],
        ['Satou Sabally', 12, 153],
    ],
    'IND': [
        ['Kelsey Mitchell', 16, 280],
        ['Tiffany Mitchell', 13, 172],
        ['Candice Dupree', 16, 202],
    ],
    'LA': [
        ['Nneka Ogwumike', 14, 172],
        ['Chelsea Gray', 16, 224],
        ['Candace Parker', 16, 211],
    ],
    'LV': [
        ['A’ja Wilson', 15, 304],
        ['Dearica Hamby', 15, 188],
        ['Angel McCoughtry', 15, 220],
    ],
    'MIN': [
        ['Napheesa Collier', 16, 262],
        ['Crystal Dangerfield', 16, 254],
    ],
    'NY': [
        ['Layshia Clarendon', 15, 188]
    ],
    'PHX': [
        ['Diana Taurasi', 13, 236],
        ['Brittney Griner', 12, 212],
        ['Skylar Diggins-Smith', 16, 261],
        ['Bria Hartley', 13, 190],
    ],
    'SEA': [
        ['Breanna Stewart', 16, 317],
        ['Jewell Loyd', 16, 223],
    ],
    'WSH': [
        ['Emma Meesseman', 13, 158],
        ['Ariel Atkins', 15, 212],
        ['Myisha Hines-Allen', 15, 236],
    ],
}

Now in JavaScript, there is a &quot;fluent&quot; or method-chaining style readily apparent:
function topTenScorers(stats) {
  return Object.entries(stats)
    .flatMap(([team, players]) =&gt; players.map(player =&gt; [...player, team]))
    .filter(([, games, ,]) =&gt; games &gt;= 15)
    .map(([name, games, points, team]) =&gt; ({ name, ppg: points / games, team }))
    .sort((p1, p2) =&gt; p2.ppg - p1.ppg)
    .slice(0, 10)
}

However, my Python solution (below) just doesn't satisfy the same way (I'm more of a JavaScript programmer). I've heard that Python list comprehensions are preferred to map and filter; I think that Python doesn't have a built-in flat_map, and, well, although you can do fancy things with itertools, Pythonic programs tend to, I think, be more favorable to computing intermediate expressions than to chaining. So I came up with the following:
def top_ten_scorers(stats):
    with_teams = [[*player, team]
                for (team, players) in stats.items()
                for player in players]
    with_ppg = [{'name': name, 'ppg': points/games, 'team': team}
                for [name, games, points, team] in with_teams
                if games &gt;= 15]
    return sorted(with_ppg, key=lambda k: k['ppg'], reverse=True)[:10]

I'd love to know whether the code is in the style of current Python best practices. I know Python is well-loved by data scientists, and this problem, although very contrived, feels data-sciencey to me, so I figured a set of best practices would have arisen that my code might not meet. Also, I'm having trouble with names for the intermediate expressions, and am not sure whether the breakdown of steps is too coarse or too fine. I'm not sure which approach to take to clean it up.
Of course, it is not imperative that a streaming solution be found; what is most important is a solution that best fits the Zen of Python rule(s) &quot;There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch.&quot;
This is more &quot;streaming&quot; in a sense:
from heapq import nlargest
from operator import itemgetter

def top_ten_scorers(stats):
    players = (dict(name=name, ppg=points/games, team=team)
               for team, players in stats.items()
               for name, games, points in players
               if games &gt;= 15)
    return nlargest(10, players, key=itemgetter('ppg'))

Your with_teams and with_ppg are fully computed lists, and then sorted creates another one that it then sorts, and then you throw away all but ten elements of it.
My players is a generator iterator, computing more elements on the fly as requested. The players = ... assignment only sets up the iterator, but nothing gets processed yet.
Then nlargest consumes players one by one, keeping only the top 10 seen so far and returning them sorted (in descending order). Could also be more efficient than sorting everything, depending on the number of eligible players.
I actually found your first two steps more confusing than helpful, as your with_teams creates an intermediate result/format to understand. I think it's simpler and easier to read to just directly produce the player dicts from the stats. Then again, I might be biased towards this and away from yours because I'm used to Python, which, as you say, isn't much into chaining.
Btw, here's an old message from Guido about some forms of chaining. Not sure it relates to what we have here, but perhaps interesting anyway.
I used dict(...) just for brevity, but {...} is faster, so you might want to keep the latter:
Setup:
name, ppg, team = 'Betnijah Laney', 263/16, 'ATL'

Round 1:
  347.041 ns  dict(name=name, ppg=ppg, team=team)
  128.325 ns  {'name': name, 'ppg': ppg, 'team': team}

Round 2:
  350.576 ns  dict(name=name, ppg=ppg, team=team)
  129.106 ns  {'name': name, 'ppg': ppg, 'team': team}

Round 3:
  347.753 ns  dict(name=name, ppg=ppg, team=team)
  130.734 ns  {'name': name, 'ppg': ppg, 'team': team}